Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
445 changes: 291 additions & 154 deletions packages/diagram-client/src/chat-panel-integrated.ts

Large diffs are not rendered by default.

141 changes: 111 additions & 30 deletions packages/diagram-client/src/chat-panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -545,50 +545,78 @@
font-size: 12px;
}

/* ── The run's agents, as the chat's read-only viewer ── */
.chat-run {
margin: 8px 16px;
/* ── Session | Run: which conversation the panel shows ── */
.chat-segments {
display: inline-flex;
border: 1px solid var(--chat-border);
border-radius: 4px;
overflow: hidden;
flex: none;
}
.chat-run-head {
display: flex;
.chat-segment {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 6px 10px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.04em;
gap: 6px;
padding: 2px 10px;
border: none;
background: transparent;
color: var(--vscode-foreground);
font-size: 11.5px;
cursor: pointer;
opacity: 0.8;
border-bottom: 1px solid var(--chat-border);
}
.chat-run-live {
margin-left: auto;
font-size: 10px;
padding: 1px 6px;
.chat-segment + .chat-segment {
border-left: 1px solid var(--chat-border);
}
.chat-segment:hover:not(:disabled) {
opacity: 1;
background: var(--vscode-toolbar-hoverBackground, rgba(128, 128, 128, 0.15));
}
.chat-segment.active {
opacity: 1;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
.chat-segment:disabled {
opacity: 0.4;
cursor: default;
}
.chat-segment-badge {
min-width: 16px;
padding: 0 5px;
border-radius: 8px;
background: var(--vscode-testing-iconPassed, #3fb950);
color: var(--vscode-editor-background, #1e1e1e);
text-transform: none;
letter-spacing: 0;
background: var(--vscode-inputValidation-infoBorder, #007acc);
color: #fff;
font-size: 10px;
line-height: 16px;
text-align: center;
}
.chat-run-empty {
padding: 8px 10px;
font-size: 12px;
opacity: 0.7;
.chat-segment.active .chat-segment-badge {
background: var(--vscode-button-foreground);
color: var(--vscode-button-background);
}
.chat-run-agent {
padding: 8px 10px;
border-bottom: 1px solid var(--chat-border);
.chat-segment-live {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--vscode-testing-iconPassed, #3fb950);
}
.chat-agent-select {
min-width: 160px;
max-width: 260px;
}
.chat-run-agent:last-child {
border-bottom: none;

/* ── A running agent's transcript, read only ── */
.chat-run-transcript {
padding: 4px 16px 8px;
}
.chat-run-agent-head {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
padding: 6px 0;
border-bottom: 1px solid var(--chat-border);
margin-bottom: 4px;
}
.chat-run-dot {
Expand All @@ -611,13 +639,66 @@
font-size: 11px;
opacity: 0.7;
}
.chat-run-empty {
padding: 8px 0;
font-size: 12px;
opacity: 0.7;
}
.chat-run-turn {
display: flex;
align-items: center;
gap: 8px;
margin: 10px 0 4px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.6;
}
.chat-run-turn::before,
.chat-run-turn::after {
content: '';
flex: 1;
border-top: 1px solid var(--chat-border);
}
.chat-run-transcript .chat-question {
margin: 8px 0;
}
.chat-run-text {
white-space: pre-wrap;
word-break: break-word;
font-size: 12px;
line-height: 1.4;
max-height: 200px;
overflow-y: auto;
margin: 4px 0;
}
.chat-viewer-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-top: 1px solid var(--chat-border);
font-size: 12px;
opacity: 0.9;
}
.chat-viewer-bar span:nth-child(2) {
flex: 1;
}
.chat-run-banner {
display: flex;
align-items: center;
gap: 8px;
margin: 0 16px 6px;
padding: 6px 10px;
border: 1px solid var(--vscode-inputValidation-infoBorder, #007acc);
border-left-width: 3px;
border-radius: 4px;
background: color-mix(in srgb, var(--vscode-inputValidation-infoBackground, #063b49) 60%, transparent);
font-size: 12px;
}
.chat-run-banner-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* ── Composer ── */
Expand Down
1 change: 1 addition & 0 deletions packages/diagram-client/src/diagram-client.css
Original file line number Diff line number Diff line change
Expand Up @@ -3186,6 +3186,7 @@ details[open] > summary .agent-chat-expand-arrow {
gap: 6px;
}
.wf-rab-empty { opacity: 0.6; padding: 8px; }
.wf-rab-card { cursor: pointer; }
.wf-rab-card {
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
Expand Down
127 changes: 116 additions & 11 deletions packages/diagram-client/src/editing-action-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -939,14 +939,40 @@ export interface RunStreamEvent {
[key: string]: unknown;
}

/** A running agent's question to the user (the run driver's human port). */
export interface LiveAgentQuestion {
id: number | string;
agent: string;
model?: string;
question: string;
context?: string;
choices: string[];
resolved?: { answer?: string; declined?: boolean };
}

/** One entry of an agent's transcript, in arrival order. */
export type LiveAgentPart =
| { kind: 'turn'; index: number }
| { kind: 'reasoning'; text: string }
| { kind: 'text'; text: string }
| { kind: 'tool'; name: string; status?: string; id?: string }
| { kind: 'question'; question: LiveAgentQuestion };

export interface LiveAgentState {
instance: string;
/** The current turn's text, reasoning and tool calls (the bar's summary). */
text: string;
reasoning: string;
status: 'running' | 'done';
toolCalls: string[];
lastSeq: number;
updatedAt: number;
/** Turns (firings) seen so far. */
turns: number;
/** The whole run's transcript of this agent: every turn, in order. */
parts: LiveAgentPart[];
/** Questions of this agent still waiting for an answer. */
pendingQuestions: number;
}


Expand All @@ -964,6 +990,53 @@ export class RunAgentStreamActionHandler implements IActionHandler {
return RunAgentStreamActionHandler.runActive;
}

static getAgent(instance: string): LiveAgentState | undefined {
return RunAgentStreamActionHandler.agents.get(instance);
}

/** Questions of every agent still waiting for an answer. */
static pendingQuestionCount(): number {
let n = 0;
for (const a of RunAgentStreamActionHandler.agents.values()) n += a.pendingQuestions;
return n;
}

/** A running agent's question, into its transcript (the agent is created
* when its stream has not reached the client yet). */
static addQuestion(instance: string, question: LiveAgentQuestion): void {
const state = RunAgentStreamActionHandler.ensure(instance);
state.parts.push({ kind: 'question', question });
state.pendingQuestions += 1;
state.updatedAt = Date.now();
RunAgentStreamActionHandler.notify();
}

/** The user's answer, onto the question in the transcript. */
static resolveQuestion(instance: string, id: number | string, resolved: { answer?: string; declined?: boolean }): void {
const state = RunAgentStreamActionHandler.agents.get(instance);
if (!state) return;
for (const part of state.parts) {
if (part.kind === 'question' && part.question.id === id && !part.question.resolved) {
part.question.resolved = resolved;
state.pendingQuestions = Math.max(0, state.pendingQuestions - 1);
break;
}
}
RunAgentStreamActionHandler.notify();
}

private static ensure(instance: string): LiveAgentState {
let state = RunAgentStreamActionHandler.agents.get(instance);
if (!state) {
state = {
instance, text: '', reasoning: '', status: 'running', toolCalls: [], lastSeq: 0, updatedAt: 0,
turns: 0, parts: [], pendingQuestions: 0
};
RunAgentStreamActionHandler.agents.set(instance, state);
}
return state;
}

/** Clear all live state (e.g. when a new run starts or the panel is dismissed). */
static reset(): void {
RunAgentStreamActionHandler.agents.clear();
Expand Down Expand Up @@ -1029,33 +1102,65 @@ export class RunAgentStreamActionHandler implements IActionHandler {
if (!instance) {
return;
}
let state = store.get(instance);
if (!state) {
state = { instance, text: '', reasoning: '', status: 'running', toolCalls: [], lastSeq: 0, updatedAt: 0 };
store.set(instance, state);
}
const state = RunAgentStreamActionHandler.ensure(instance);
state.lastSeq = typeof ev.seq === 'number' ? ev.seq : state.lastSeq;
state.updatedAt = Date.now();
const last = state.parts[state.parts.length - 1];
switch (type) {
case 'agent.message.start':
// New firing: reset the in-progress message so the view shows the current turn.
state.status = 'running';
state.text = '';
state.reasoning = '';
state.toolCalls = [];
state.turns += 1;
state.parts.push({ kind: 'turn', index: state.turns });
break;
case 'agent.message.delta':
case 'agent.message.delta': {
const delta = String(ev.delta ?? '');
if (ev.field === 'reasoning') {
state.reasoning += String(ev.delta ?? '');
state.reasoning += delta;
if (last && last.kind === 'reasoning') last.text += delta;
else state.parts.push({ kind: 'reasoning', text: delta });
} else {
state.text += String(ev.delta ?? '');
state.text += delta;
if (last && last.kind === 'text') last.text += delta;
else state.parts.push({ kind: 'text', text: delta });
}
break;
case 'agent.tool_call':
if (ev.name) {
state.toolCalls.push(String(ev.name));
}
case 'agent.tool_call': {
// The runtime names the call (`name`); an ACP agent's call has
// a title ("Write choice-a.txt") where the name may be missing.
const label = ev.name ?? ev.title ?? ev.kind;
if (label) {
state.toolCalls.push(String(label));
state.parts.push({
kind: 'tool', name: String(label),
status: typeof ev.status === 'string' ? ev.status : undefined,
id: ev.id !== undefined && ev.id !== null ? String(ev.id) : undefined
});
}
break;
}
case 'agent.tool_call_update': {
// The call it updates: by id, else the latest call.
const id = ev.id !== undefined && ev.id !== null ? String(ev.id) : undefined;
let target: Extract<LiveAgentPart, { kind: 'tool' }> | undefined;
for (let i = state.parts.length - 1; i >= 0; i--) {
const p = state.parts[i];
if (p.kind === 'tool' && (id === undefined || p.id === id)) {
target = p;
break;
}
}
if (target) {
if (typeof ev.status === 'string') target.status = ev.status;
const label = ev.name ?? ev.title;
if (label && (target.name === 'tool' || target.name === target.status)) target.name = String(label);
}
break;
}
case 'agent.message.end':
state.status = 'done';
break;
Expand Down
14 changes: 12 additions & 2 deletions packages/diagram-client/src/running-agents-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ import { RunAgentStreamActionHandler, type LiveAgentState } from './editing-acti
export class RunningAgentsBar implements IDiagramStartup {
private host?: HTMLElement;
private scheduled = false;
/** The chat panel shows a running agent: one place for the run, not two. */
private behindChat = false;

postModelInitialization(): void {
this.ensureHost();
window.addEventListener('dialogram.runAgents.updated', () => this.scheduleRender());
window.addEventListener('dialogram.chat.runView', (e) => {
this.behindChat = Boolean((e as CustomEvent<{ visible?: boolean }>).detail?.visible);
this.scheduleRender();
});
this.scheduleRender();
// eslint-disable-next-line no-console
console.log('[wf-lang overlay] RunningAgentsBar mounted; listening for dialogram.runAgents.updated');
Expand Down Expand Up @@ -55,7 +61,7 @@ export class RunningAgentsBar implements IDiagramStartup {
}
const agents = RunAgentStreamActionHandler.getAgents();
const active = RunAgentStreamActionHandler.isRunActive();
const visible = active || agents.length > 0;
const visible = !this.behindChat && (active || agents.length > 0);
// eslint-disable-next-line no-console
console.log(`[wf-lang overlay] RunningAgentsBar render: agents=${agents.length} active=${active} visible=${visible}`);
this.host.classList.toggle('hidden', !visible);
Expand Down Expand Up @@ -96,7 +102,11 @@ export class RunningAgentsBar implements IDiagramStartup {

private card(a: LiveAgentState): TemplateResult {
return html`
<div class="wf-rab-card ${a.status}">
<div
class="wf-rab-card ${a.status}"
title="Open in the chat"
@click=${() => window.dispatchEvent(new CustomEvent('dialogram.chat.showAgent', { detail: { instance: a.instance } }))}
>
<div class="wf-rab-card-head">
<span class="wf-rab-dot ${a.status}"></span>
<span class="wf-rab-name" title=${a.instance}>${a.instance}</span>
Expand Down
Loading
Loading