Skip to content
This repository was archived by the owner on Jun 28, 2026. It is now read-only.

Commit bc2b323

Browse files
olasunkanmi.raymondolasunkanmi.raymond
authored andcommitted
feat(team-graph, webview-ui): Introduce team intelligence graph and audit documentation
* Add for a comprehensive WebviewUI deep-dive. * Implement to expose team intelligence data from the extension. * Develop webview component for visualizing team members, relationships, and recurring blockers. * Integrate into the main webview UI with a new sidebar toggle button. * Create Zustand store to manage team graph UI state and interactions.
1 parent acc272e commit bc2b323

11 files changed

Lines changed: 1661 additions & 0 deletions

File tree

docs/WEBVIEW_UI_AUDIT.md

Lines changed: 317 additions & 0 deletions
Large diffs are not rendered by default.

src/webview-providers/base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
SessionHandler,
6969
SettingsHandler,
7070
StandupHandler,
71+
TeamGraphHandler,
7172
DoctorHandler,
7273
OnboardingHandler,
7374
} from "./handlers";
@@ -268,6 +269,7 @@ export abstract class BaseWebViewProvider implements vscode.Disposable {
268269
this.handlerRegistry.register(new CheckpointHandler());
269270
this.handlerRegistry.register(new ComposerHandler());
270271
this.handlerRegistry.register(new StandupHandler());
272+
this.handlerRegistry.register(new TeamGraphHandler());
271273
this.handlerRegistry.register(new DoctorHandler());
272274
this.handlerRegistry.register(new OnboardingHandler());
273275
this.handlerRegistry.register(

src/webview-providers/handlers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ export { PerformanceHandler } from "./performance-handler";
1919
export { CheckpointHandler } from "./checkpoint-handler";
2020
export { ComposerHandler } from "./composer-handler";
2121
export { StandupHandler } from "./standup-handler";
22+
export { TeamGraphHandler } from "./team-graph-handler";
2223
export { DoctorHandler } from "./doctor-handler";
2324
export { OnboardingHandler } from "./onboarding-handler";
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import { WebviewMessageHandler, HandlerContext } from "./types";
2+
import { TeamGraphStore } from "../../services/team-graph-store";
3+
import type {
4+
PersonProfile,
5+
Relationship,
6+
} from "../../services/team-graph-store";
7+
8+
// ── Message types ──────────────────────────────────────────────
9+
10+
type TeamHydrateMessage = { command: "team-hydrate" };
11+
type TeamPersonProfileMessage = {
12+
command: "team-person-profile";
13+
name: string;
14+
};
15+
type TeamHealthMessage = { command: "team-health" };
16+
type TeamRelationshipsMessage = {
17+
command: "team-relationships";
18+
name?: string;
19+
};
20+
type TeamBlockersMessage = { command: "team-recurring-blockers" };
21+
type TeamCommitmentsMessage = {
22+
command: "team-commitments";
23+
name: string;
24+
};
25+
26+
type TeamGraphMessage =
27+
| TeamHydrateMessage
28+
| TeamPersonProfileMessage
29+
| TeamHealthMessage
30+
| TeamRelationshipsMessage
31+
| TeamBlockersMessage
32+
| TeamCommitmentsMessage;
33+
34+
const TEAM_GRAPH_COMMANDS = [
35+
"team-hydrate",
36+
"team-person-profile",
37+
"team-health",
38+
"team-relationships",
39+
"team-recurring-blockers",
40+
"team-commitments",
41+
] as const;
42+
43+
function isTeamGraphMessage(msg: unknown): msg is TeamGraphMessage {
44+
return (
45+
typeof msg === "object" &&
46+
msg !== null &&
47+
"command" in msg &&
48+
typeof (msg as Record<string, unknown>).command === "string" &&
49+
TEAM_GRAPH_COMMANDS.includes(
50+
(msg as Record<string, unknown>)
51+
.command as (typeof TEAM_GRAPH_COMMANDS)[number],
52+
)
53+
);
54+
}
55+
56+
/** Serializable person for the webview. */
57+
interface TeamMember {
58+
id: number;
59+
name: string;
60+
role: string | null;
61+
expertise: string[];
62+
workStyle: string | null;
63+
standupCount: number;
64+
commitmentCount: number;
65+
completionCount: number;
66+
completionRate: number;
67+
firstSeen: string;
68+
lastSeen: string;
69+
}
70+
71+
interface TeamRelationshipEdge {
72+
sourceName: string;
73+
targetName: string;
74+
kind: string;
75+
weight: number;
76+
}
77+
78+
function personToMember(p: PersonProfile): TeamMember {
79+
return {
80+
id: p.id,
81+
name: p.name,
82+
role: p.role,
83+
expertise: p.traits.expertise ?? [],
84+
workStyle: p.traits.workStyle ?? null,
85+
standupCount: p.standup_count,
86+
commitmentCount: p.commitment_count,
87+
completionCount: p.completion_count,
88+
completionRate:
89+
p.commitment_count > 0
90+
? Math.round((p.completion_count / p.commitment_count) * 100)
91+
: 0,
92+
firstSeen: p.first_seen,
93+
lastSeen: p.last_seen,
94+
};
95+
}
96+
97+
export class TeamGraphHandler implements WebviewMessageHandler {
98+
readonly commands = [...TEAM_GRAPH_COMMANDS];
99+
100+
async handle(message: unknown, ctx: HandlerContext): Promise<void> {
101+
if (!isTeamGraphMessage(message)) {
102+
ctx.logger.warn("TeamGraphHandler received invalid message shape");
103+
return;
104+
}
105+
106+
try {
107+
const store = TeamGraphStore.getInstance();
108+
await store.ensureInitialized();
109+
110+
switch (message.command) {
111+
case "team-hydrate": {
112+
const people = store.getAllPeople();
113+
const members = people.map(personToMember);
114+
115+
// Get all collaboration relationships for the graph
116+
const edges: TeamRelationshipEdge[] = [];
117+
const seenEdges = new Set<string>();
118+
for (const person of people) {
119+
const rels = store.getRelationshipsFor(person.id);
120+
for (const rel of rels) {
121+
const key = [
122+
Math.min(rel.source_person_id, rel.target_person_id),
123+
Math.max(rel.source_person_id, rel.target_person_id),
124+
rel.kind,
125+
].join("-");
126+
if (seenEdges.has(key)) continue;
127+
seenEdges.add(key);
128+
129+
const src = people.find((p) => p.id === rel.source_person_id);
130+
const tgt = people.find((p) => p.id === rel.target_person_id);
131+
if (src && tgt) {
132+
edges.push({
133+
sourceName: src.name,
134+
targetName: tgt.name,
135+
kind: rel.kind,
136+
weight: rel.weight,
137+
});
138+
}
139+
}
140+
}
141+
142+
// Health stats
143+
const healthMarkdown = store.getTeamHealth();
144+
145+
await ctx.webview.webview.postMessage({
146+
command: "team-hydrate-result",
147+
members,
148+
edges,
149+
health: healthMarkdown,
150+
});
151+
break;
152+
}
153+
154+
case "team-person-profile": {
155+
if (!message.name || typeof message.name !== "string") {
156+
await ctx.webview.webview.postMessage({
157+
command: "team-person-profile-result",
158+
error: "No person name provided.",
159+
});
160+
return;
161+
}
162+
const profileMarkdown = store.getPersonProfile(message.name);
163+
const person = store.getPersonByName(message.name);
164+
const member = person ? personToMember(person) : null;
165+
166+
// Get commitments
167+
let commitments: Array<{
168+
action: string;
169+
status: string;
170+
date: string;
171+
}> = [];
172+
if (person) {
173+
commitments = store.getCommitmentsFor(person.id, 10).map((c) => ({
174+
action: c.action,
175+
status: c.status,
176+
date: c.date,
177+
}));
178+
}
179+
180+
// Get collaborators
181+
let collaborators: Array<{ name: string; weight: number }> = [];
182+
if (person) {
183+
collaborators = store
184+
.getTopCollaborators(person.id, 5)
185+
.map((c) => ({
186+
name: c.person.name,
187+
weight: c.weight,
188+
}));
189+
}
190+
191+
await ctx.webview.webview.postMessage({
192+
command: "team-person-profile-result",
193+
member,
194+
commitments,
195+
collaborators,
196+
profileMarkdown,
197+
});
198+
break;
199+
}
200+
201+
case "team-health": {
202+
const healthMarkdown = store.getTeamHealth();
203+
await ctx.webview.webview.postMessage({
204+
command: "team-health-result",
205+
health: healthMarkdown,
206+
});
207+
break;
208+
}
209+
210+
case "team-relationships": {
211+
const people = store.getAllPeople();
212+
const edges: TeamRelationshipEdge[] = [];
213+
const seenEdges = new Set<string>();
214+
215+
// If a specific person requested, only their relationships
216+
const targetPeople = message.name
217+
? people.filter(
218+
(p) => p.name.toLowerCase() === message.name!.toLowerCase(),
219+
)
220+
: people;
221+
222+
for (const person of targetPeople) {
223+
const rels = store.getRelationshipsFor(person.id);
224+
for (const rel of rels) {
225+
const key = [
226+
Math.min(rel.source_person_id, rel.target_person_id),
227+
Math.max(rel.source_person_id, rel.target_person_id),
228+
rel.kind,
229+
].join("-");
230+
if (seenEdges.has(key)) continue;
231+
seenEdges.add(key);
232+
233+
const src = people.find((p) => p.id === rel.source_person_id);
234+
const tgt = people.find((p) => p.id === rel.target_person_id);
235+
if (src && tgt) {
236+
edges.push({
237+
sourceName: src.name,
238+
targetName: tgt.name,
239+
kind: rel.kind,
240+
weight: rel.weight,
241+
});
242+
}
243+
}
244+
}
245+
246+
await ctx.webview.webview.postMessage({
247+
command: "team-relationships-result",
248+
edges,
249+
});
250+
break;
251+
}
252+
253+
case "team-recurring-blockers": {
254+
const blockersMarkdown = store.getRecurringBlockers();
255+
await ctx.webview.webview.postMessage({
256+
command: "team-recurring-blockers-result",
257+
blockers: blockersMarkdown,
258+
});
259+
break;
260+
}
261+
262+
case "team-commitments": {
263+
if (!message.name || typeof message.name !== "string") {
264+
await ctx.webview.webview.postMessage({
265+
command: "team-commitments-result",
266+
error: "No person name provided.",
267+
});
268+
return;
269+
}
270+
const person = store.getPersonByName(message.name);
271+
if (!person) {
272+
await ctx.webview.webview.postMessage({
273+
command: "team-commitments-result",
274+
error: `No person found: "${message.name}"`,
275+
});
276+
return;
277+
}
278+
const commitments = store
279+
.getCommitmentsFor(person.id, 20)
280+
.map((c) => ({
281+
action: c.action,
282+
status: c.status,
283+
date: c.date,
284+
deadline: c.deadline,
285+
}));
286+
287+
await ctx.webview.webview.postMessage({
288+
command: "team-commitments-result",
289+
name: person.name,
290+
commitments,
291+
});
292+
break;
293+
}
294+
}
295+
} catch (error: unknown) {
296+
const msg = error instanceof Error ? error.message : "Unknown error";
297+
ctx.logger.error(`TeamGraphHandler error: ${msg}`);
298+
try {
299+
await ctx.webview.webview.postMessage({
300+
command: "team-error",
301+
error: msg,
302+
});
303+
} catch {
304+
// webview may be disposed
305+
}
306+
}
307+
}
308+
}

0 commit comments

Comments
 (0)