Skip to content

Commit 014db0a

Browse files
mpstatonclaude
andcommitted
new(chat, org-workbench, workspace): didi knows which org is loaded — focused-entity context lands
The org workbench broadcasts its active org (augment-it:active-entity event + localStorage, the search-envelope race-hardening pattern) on every card load, clear, and workspace switch. The chat rail picks it up and rides focused_org_slug/name on every chat_turn; the context slab tells didi "the user is viewing X (org_slug: …) — 'this org' refers to it", and the crawl shortcuts accept bare /crawl-links, /crawl-streams, /crawl-team (no argument) against the focused org. This is the didi-chat plan's step-1 context plumbing, landed ahead of the team-page staging it was scoped for. Rider on #33. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UvYzx7vDWeafnkAi2nEQeb
1 parent 3fdd2e0 commit 014db0a

6 files changed

Lines changed: 96 additions & 5 deletions

File tree

apps/chat/src/ChatSurface.svelte

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,28 @@
119119
suggest(workspace.activeView, workspace.last_capability),
120120
);
121121
122+
// Focused entity — the org card open in the Org Workbench, broadcast via
123+
// augment-it:active-entity + localStorage (race-hardened like the search
124+
// envelope). Lets didi resolve "this org" without asking.
125+
const ACTIVE_ENTITY_KEY = 'augment-it:active-entity';
126+
type ActiveEntity = { type: 'organization'; org_slug: string; display_name?: string };
127+
function readActiveEntity(): ActiveEntity | null {
128+
try {
129+
const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(ACTIVE_ENTITY_KEY) : null;
130+
return raw ? (JSON.parse(raw) as ActiveEntity) : null;
131+
} catch {
132+
return null;
133+
}
134+
}
135+
let activeEntity = $state<ActiveEntity | null>(readActiveEntity());
136+
$effect(() => {
137+
const onEntity = (e: Event) => {
138+
activeEntity = ((e as CustomEvent).detail as ActiveEntity | null) ?? null;
139+
};
140+
window.addEventListener('augment-it:active-entity', onEntity);
141+
return () => window.removeEventListener('augment-it:active-entity', onEntity);
142+
});
143+
122144
// Send context — what the user is looking at right now. The server
123145
// inlines this in the prompt so the model can pick a record_set_id
124146
// for prompt.draft without asking. `client_id` is the active workspace,
@@ -128,13 +150,24 @@
128150
focused_prompt_id?: string;
129151
record_set_id?: string;
130152
client_id?: string;
153+
focused_org_slug?: string;
154+
focused_org_name?: string;
131155
} | undefined>(
132156
(() => {
133-
const ctx: { record_set_id?: string; client_id?: string } = {};
157+
const ctx: {
158+
record_set_id?: string;
159+
client_id?: string;
160+
focused_org_slug?: string;
161+
focused_org_name?: string;
162+
} = {};
134163
if (workspace.activeView.kind === 'record_set') {
135164
ctx.record_set_id = workspace.activeView.record_set_id;
136165
}
137166
if (workspace.active_client_id) ctx.client_id = workspace.active_client_id;
167+
if (activeEntity?.type === 'organization') {
168+
ctx.focused_org_slug = activeEntity.org_slug;
169+
if (activeEntity.display_name) ctx.focused_org_name = activeEntity.display_name;
170+
}
138171
return Object.keys(ctx).length ? ctx : undefined;
139172
})(),
140173
);

apps/chat/src/chat-state.svelte.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,16 @@ class ChatState {
4141
* The propose / invoke branches don't auto-act — the surface renders
4242
* affordances and the user clicks to accept.
4343
*/
44-
async sendMessage(message: string, context?: { focused_prompt_id?: string; record_set_id?: string; client_id?: string }): Promise<void> {
44+
async sendMessage(
45+
message: string,
46+
context?: {
47+
focused_prompt_id?: string;
48+
record_set_id?: string;
49+
client_id?: string;
50+
focused_org_slug?: string;
51+
focused_org_name?: string;
52+
},
53+
): Promise<void> {
4554
if (this.sending) return;
4655
if (!message.trim()) return;
4756
const id = `t_${Date.now().toString(36)}`;

apps/org-workbench/src/App.svelte

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@
2020
const WS_URL = 'ws://localhost:3001/ws';
2121
// Restore the last-worked org on remount (HMR, flow switch, tab reopen).
2222
const ACTIVE_ORG_KEY = 'augment-it:org-workbench:active-org';
23+
// Cross-remote focused-entity broadcast — the chat rail includes it in
24+
// every chat_turn so didi knows what "this org" means. Event + localStorage
25+
// (the search-envelope race-hardening pattern, spec D2).
26+
const ACTIVE_ENTITY_KEY = 'augment-it:active-entity';
27+
28+
function broadcastActiveEntity(
29+
detail: { type: 'organization'; org_slug: string; display_name?: string } | null,
30+
) {
31+
if (typeof localStorage !== 'undefined') {
32+
if (detail) localStorage.setItem(ACTIVE_ENTITY_KEY, JSON.stringify(detail));
33+
else localStorage.removeItem(ACTIVE_ENTITY_KEY);
34+
}
35+
window.dispatchEvent(new CustomEvent('augment-it:active-entity', { detail }));
36+
}
2337
2438
let status = $state<'connecting' | 'open' | 'closed' | 'error'>('connecting');
2539
let client = $state<string>('reach-edu');
@@ -34,9 +48,15 @@
3448
try {
3549
org = await fetchOrgDetail(org_slug, client);
3650
if (typeof localStorage !== 'undefined') localStorage.setItem(ACTIVE_ORG_KEY, org_slug);
51+
broadcastActiveEntity({
52+
type: 'organization',
53+
org_slug: org.slug,
54+
display_name: org.complete_name ?? org.conventional_name ?? org.slug,
55+
});
3756
} catch (err) {
3857
error = err instanceof Error ? err.message : String(err);
3958
org = null;
59+
broadcastActiveEntity(null);
4060
} finally {
4161
loading = false;
4262
}
@@ -73,6 +93,7 @@
7393
// A different client sees a different slice of the canonical layer —
7494
// drop the card rather than show rows the new client may not access.
7595
org = null;
96+
broadcastActiveEntity(null);
7697
}
7798
7899
async function loadActiveClient() {

packages/workspace/src/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,16 @@ export type ChatTurnFrame = {
269269
* the wire field is `client_id` (lived-with naming with the prior
270270
* substrate plan; the UI/spec word is "workspace").
271271
*/
272-
context?: { focused_prompt_id?: string; record_set_id?: string; client_id?: string };
272+
context?: {
273+
focused_prompt_id?: string;
274+
record_set_id?: string;
275+
client_id?: string;
276+
// The org card open in the Org Workbench — broadcast via
277+
// augment-it:active-entity (+ localStorage, same race-hardening as the
278+
// search envelope) so didi's "this org" resolves without asking.
279+
focused_org_slug?: string;
280+
focused_org_name?: string;
281+
};
273282
};
274283

275284
// One workspace as discovered by the workspace-service. The directory

services/workspace/src/chat.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ VERB RECOGNITION SHORTCUTS:
127127
- "/crawl-links <org-slug>" → chat_invoke organization.crawl with target "links".
128128
- "/crawl-streams <org-slug>" → chat_invoke organization.crawl with target "streams".
129129
- "/crawl-team <org-slug>" → chat_invoke organization.crawl with target "team".
130+
- A bare "/crawl-links" / "/crawl-streams" / "/crawl-team" (no argument), or "this org" / "this organization", targets the focused organization from context when one is present — chat_invoke directly with its org_slug. No focused org and no argument → ask.
130131
- Natural phrasings ("crawl for relevant pulse streams for X", "find X's team members") map to the same targets — chat_invoke when the org is unambiguous, chat_propose otherwise.
131132
`;
132133

@@ -221,7 +222,14 @@ export const CHAT_TOOLS = [
221222
export type ChatTurnInput = {
222223
message: string;
223224
thread?: { role: 'user' | 'assistant'; content: string }[];
224-
context?: { focused_prompt_id?: string; record_set_id?: string; client_id?: string };
225+
context?: {
226+
focused_prompt_id?: string;
227+
record_set_id?: string;
228+
client_id?: string;
229+
// The org card open in the Org Workbench — "this org" resolves to it.
230+
focused_org_slug?: string;
231+
focused_org_name?: string;
232+
};
225233
suggestions?: { capability: string; hint: string }[];
226234
};
227235

@@ -262,6 +270,11 @@ function contextSlab(ctx?: ChatTurnInput['context']): string {
262270
}
263271
if (ctx?.focused_prompt_id) parts.push(`The user is currently looking at prompt: ${ctx.focused_prompt_id}`);
264272
if (ctx?.record_set_id) parts.push(`The user is currently in record set: ${ctx.record_set_id}`);
273+
if (ctx?.focused_org_slug) {
274+
parts.push(
275+
`The user is currently viewing the organization "${ctx.focused_org_name ?? ctx.focused_org_slug}" (org_slug: ${ctx.focused_org_slug}) in the Org Workbench. "This org" / "this organization" refers to it — use this org_slug for organization.crawl and any org-scoped capability.`,
276+
);
277+
}
265278
return parts.join('\n') + '\n';
266279
}
267280

services/workspace/src/ws.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,13 @@ export async function registerWebsocket(app: FastifyInstance): Promise<void> {
156156
via?: string;
157157
message?: string;
158158
thread_id?: string;
159-
context?: { focused_prompt_id?: string; record_set_id?: string; client_id?: string };
159+
context?: {
160+
focused_prompt_id?: string;
161+
record_set_id?: string;
162+
client_id?: string;
163+
focused_org_slug?: string;
164+
focused_org_name?: string;
165+
};
160166
thread?: { role: 'user' | 'assistant'; content: string }[];
161167
suggestions?: { capability: string; hint: string }[];
162168
};

0 commit comments

Comments
 (0)