@mjasnikovs/pi-task 0.17.21 → 0.17.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/remote/bridge.d.ts +8 -0
- package/dist/remote/bridge.js +10 -0
- package/dist/remote/events.js +10 -4
- package/dist/remote/history.d.ts +11 -1
- package/dist/remote/history.js +1 -1
- package/dist/remote/protocol.d.ts +23 -2
- package/dist/remote/protocol.js +2 -0
- package/dist/remote/register.js +7 -3
- package/dist/remote/server.d.ts +1 -1
- package/dist/remote/server.js +5 -1
- package/dist/remote/session-state.d.ts +19 -5
- package/dist/remote/session-state.js +55 -9
- package/dist/remote/ui-highlight.d.ts +2 -0
- package/dist/remote/ui-highlight.js +119 -0
- package/dist/remote/ui-render.d.ts +2 -0
- package/dist/remote/ui-render.js +173 -0
- package/dist/remote/ui-script.js +425 -146
- package/dist/remote/ui-styles.d.ts +1 -1
- package/dist/remote/ui-styles.js +175 -8
- package/dist/remote/ui-tools.d.ts +2 -0
- package/dist/remote/ui-tools.js +113 -0
- package/dist/remote/ui.js +19 -1
- package/dist/task/impl-widget.js +2 -2
- package/dist/task/widget.d.ts +9 -0
- package/dist/task/widget.js +42 -2
- package/package.json +1 -1
package/dist/remote/bridge.d.ts
CHANGED
|
@@ -65,6 +65,14 @@ export declare class SessionUI {
|
|
|
65
65
|
private askLocal;
|
|
66
66
|
}
|
|
67
67
|
export declare function publishNotify(message: string, level: 'info' | 'warning' | 'error'): void;
|
|
68
|
+
/**
|
|
69
|
+
* Abort the running agent turn — the browser Stop button. `abort()` lives on the
|
|
70
|
+
* base ExtensionContext ("Abort the current agent operation") so it works for a
|
|
71
|
+
* host chat turn, a /task phase turn, or a /task-auto run alike. The shimmed ctx
|
|
72
|
+
* inherits it from the real event ctx via its prototype, so this is safe even
|
|
73
|
+
* before the user has run a command in the terminal. No-op when nothing is running.
|
|
74
|
+
*/
|
|
75
|
+
export declare function interruptAgent(): void;
|
|
68
76
|
/**
|
|
69
77
|
* Mirror a task lifecycle notice (the kind pi-task shows on the terminal via
|
|
70
78
|
* ctx.ui.notify) to connected remote viewers. Task failures and other
|
package/dist/remote/bridge.js
CHANGED
|
@@ -108,6 +108,16 @@ export class SessionUI {
|
|
|
108
108
|
export function publishNotify(message, level) {
|
|
109
109
|
getBridge().broadcast({ type: 'notify', message, level });
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Abort the running agent turn — the browser Stop button. `abort()` lives on the
|
|
113
|
+
* base ExtensionContext ("Abort the current agent operation") so it works for a
|
|
114
|
+
* host chat turn, a /task phase turn, or a /task-auto run alike. The shimmed ctx
|
|
115
|
+
* inherits it from the real event ctx via its prototype, so this is safe even
|
|
116
|
+
* before the user has run a command in the terminal. No-op when nothing is running.
|
|
117
|
+
*/
|
|
118
|
+
export function interruptAgent() {
|
|
119
|
+
getBridge().currentCtx?.abort();
|
|
120
|
+
}
|
|
111
121
|
/**
|
|
112
122
|
* Mirror a task lifecycle notice (the kind pi-task shows on the terminal via
|
|
113
123
|
* ctx.ui.notify) to connected remote viewers. Task failures and other
|
package/dist/remote/events.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { setAgentIdle } from './state.js';
|
|
2
2
|
import { publishNotify } from './bridge.js';
|
|
3
|
-
import { agentStart, appendText, textEnd, startTool, updateTool, endTool, agentEnd, addUserTurn, addError, addSystemNote } from './session-state.js';
|
|
3
|
+
import { agentStart, appendText, textEnd, appendThinking, thinkingEnd, startTool, updateTool, endTool, agentEnd, addUserTurn, addError, addSystemNote } from './session-state.js';
|
|
4
4
|
/** Mirror pi agent events into the authoritative SessionState. Each handler
|
|
5
5
|
* drives a mutator, which updates the snapshot AND broadcasts the live delta. */
|
|
6
6
|
export function setupEvents(pi) {
|
|
7
|
-
pi.on('agent_start', (_event,
|
|
7
|
+
pi.on('agent_start', (_event, ctx) => {
|
|
8
8
|
setAgentIdle(false);
|
|
9
|
-
agentStart();
|
|
9
|
+
agentStart(ctx.model?.name);
|
|
10
10
|
});
|
|
11
11
|
pi.on('message_update', (event, _ctx) => {
|
|
12
12
|
const ae = event.assistantMessageEvent;
|
|
13
13
|
if (ae.type === 'text_delta' && 'delta' in ae && typeof ae.delta === 'string') {
|
|
14
14
|
appendText(ae.delta);
|
|
15
15
|
}
|
|
16
|
+
else if (ae.type === 'thinking_delta' && 'delta' in ae && typeof ae.delta === 'string') {
|
|
17
|
+
appendThinking(ae.delta);
|
|
18
|
+
}
|
|
19
|
+
else if (ae.type === 'thinking_end') {
|
|
20
|
+
thinkingEnd();
|
|
21
|
+
}
|
|
16
22
|
else if (ae.type === 'error') {
|
|
17
23
|
const errorMessage = 'error' in ae && ae.error && typeof ae.error.errorMessage === 'string' ?
|
|
18
24
|
ae.error.errorMessage
|
|
@@ -43,7 +49,7 @@ export function setupEvents(pi) {
|
|
|
43
49
|
});
|
|
44
50
|
pi.on('agent_end', (_event, ctx) => {
|
|
45
51
|
setAgentIdle(true);
|
|
46
|
-
agentEnd(ctx.getContextUsage());
|
|
52
|
+
agentEnd(ctx.getContextUsage(), ctx.model?.name);
|
|
47
53
|
// Deliberately no push: agent_end fires on EVERY host turn — every chat
|
|
48
54
|
// reply, every internal phase turn inside /task, and every internal /task
|
|
49
55
|
// run inside /task-auto — so a "Task finished" push here floods the device.
|
package/dist/remote/history.d.ts
CHANGED
|
@@ -2,6 +2,12 @@ export interface TextPart {
|
|
|
2
2
|
kind: 'text';
|
|
3
3
|
text: string;
|
|
4
4
|
}
|
|
5
|
+
export interface ThinkingPart {
|
|
6
|
+
kind: 'thinking';
|
|
7
|
+
text: string;
|
|
8
|
+
/** false while thinking deltas are still streaming; true once thinking_end fires. */
|
|
9
|
+
done?: boolean;
|
|
10
|
+
}
|
|
5
11
|
export interface ToolPart {
|
|
6
12
|
kind: 'tool';
|
|
7
13
|
toolCallId: string;
|
|
@@ -11,8 +17,10 @@ export interface ToolPart {
|
|
|
11
17
|
isError: boolean;
|
|
12
18
|
/** false while the tool is still running (result not in yet). */
|
|
13
19
|
done: boolean;
|
|
20
|
+
/** Wall-clock duration once finished, shown dimly in the tool summary. */
|
|
21
|
+
elapsedMs?: number;
|
|
14
22
|
}
|
|
15
|
-
export type Part = TextPart | ToolPart;
|
|
23
|
+
export type Part = TextPart | ThinkingPart | ToolPart;
|
|
16
24
|
export interface Turn {
|
|
17
25
|
role: 'user' | 'assistant' | 'system';
|
|
18
26
|
/** User text, error text, or a system note. Assistant content lives in `parts`. */
|
|
@@ -20,6 +28,8 @@ export interface Turn {
|
|
|
20
28
|
/** Ordered assistant content (text + tools). */
|
|
21
29
|
parts?: Part[];
|
|
22
30
|
error?: boolean;
|
|
31
|
+
/** Epoch ms when the turn was committed — the client renders a dim HH:MM. */
|
|
32
|
+
ts?: number;
|
|
23
33
|
}
|
|
24
34
|
export declare class HistoryBuffer {
|
|
25
35
|
private entries;
|
package/dist/remote/history.js
CHANGED
|
@@ -10,10 +10,27 @@ export interface PromptResolvedMessage {
|
|
|
10
10
|
type: 'prompt_resolved';
|
|
11
11
|
id: string;
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
13
|
+
/** Structured task-widget payload — lets the browser render a real progress bar,
|
|
14
|
+
* phase badge and elapsed clock instead of reflowing the terminal's text lines.
|
|
15
|
+
* Always sent ALONGSIDE `lines`, which stays the authoritative fallback. */
|
|
16
|
+
export interface WidgetData {
|
|
17
|
+
/** Head line, e.g. "TASK_0003 · auth routes" or "/task-auto · building". */
|
|
18
|
+
title: string;
|
|
19
|
+
/** Current phase/stage label, e.g. "refine", "implementing", "enforcing guidelines". */
|
|
20
|
+
phase: string;
|
|
21
|
+
/** Progress numerator (current step) — omitted for stages without numbering. */
|
|
22
|
+
done?: number;
|
|
23
|
+
/** Progress denominator (total steps) — omitted for stages without numbering. */
|
|
24
|
+
total?: number;
|
|
25
|
+
/** Pre-formatted elapsed clock, e.g. "2:14". */
|
|
26
|
+
elapsed?: string;
|
|
27
|
+
}
|
|
28
|
+
/** The single task-widget slot. `lines: null` clears it. `data` carries the
|
|
29
|
+
* structured view (null when clearing or when a producer only has text). */
|
|
14
30
|
export interface WidgetMessage {
|
|
15
31
|
type: 'widget';
|
|
16
32
|
lines: string[] | null;
|
|
33
|
+
data?: WidgetData | null;
|
|
17
34
|
}
|
|
18
35
|
export interface NotifyMessage {
|
|
19
36
|
type: 'notify';
|
|
@@ -58,5 +75,9 @@ export interface ClientPromptAnswer {
|
|
|
58
75
|
id: string;
|
|
59
76
|
value: string | undefined;
|
|
60
77
|
}
|
|
61
|
-
|
|
78
|
+
/** Stop the running agent turn (the browser Stop button). */
|
|
79
|
+
export interface ClientInterrupt {
|
|
80
|
+
type: 'interrupt';
|
|
81
|
+
}
|
|
82
|
+
export type ClientMessage = ClientChatMessage | ClientPromptAnswer | ClientInterrupt;
|
|
62
83
|
export declare function isClientMessage(x: unknown): x is ClientMessage;
|
package/dist/remote/protocol.js
CHANGED
|
@@ -6,6 +6,8 @@ export function isClientMessage(x) {
|
|
|
6
6
|
const m = x;
|
|
7
7
|
if (m.type === 'message')
|
|
8
8
|
return typeof m.text === 'string';
|
|
9
|
+
if (m.type === 'interrupt')
|
|
10
|
+
return true;
|
|
9
11
|
if (m.type === 'prompt_answer') {
|
|
10
12
|
return typeof m.id === 'string' && (m.value === undefined || typeof m.value === 'string');
|
|
11
13
|
}
|
package/dist/remote/register.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getConfig } from '../config/config.js';
|
|
2
|
-
import { getBridge, dispatchRemoteLine, dispatchRemoteNewSession, makeShimmedCtx } from './bridge.js';
|
|
2
|
+
import { getBridge, dispatchRemoteLine, dispatchRemoteNewSession, makeShimmedCtx, interruptAgent } from './bridge.js';
|
|
3
3
|
import { setupEvents } from './events.js';
|
|
4
4
|
import { reset, addUserTurn } from './session-state.js';
|
|
5
5
|
import { html } from './ui.js';
|
|
@@ -33,11 +33,15 @@ export function registerRemote(pi) {
|
|
|
33
33
|
S.send?.(plain);
|
|
34
34
|
}
|
|
35
35
|
else {
|
|
36
|
-
|
|
36
|
+
// Mid-run: steer the live turn (inject the message into the
|
|
37
|
+
// current generation) rather than queueing it for after, so a
|
|
38
|
+
// remote nudge lands immediately — matching the composer's
|
|
39
|
+
// "delivered mid-run" affordance.
|
|
40
|
+
S.send?.(plain, { deliverAs: 'steer' });
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
});
|
|
40
|
-
}, wsUrl => html(wsUrl));
|
|
44
|
+
}, wsUrl => html(wsUrl), interruptAgent);
|
|
41
45
|
// Hands-off HTTPS: point Tailscale serve at our port so phones get a
|
|
42
46
|
// secure context. Best-effort — any failure degrades to the http URL.
|
|
43
47
|
S.serveResult = await ensureTailscaleServe(S.server.port).catch(() => ({ state: 'unavailable' }));
|
package/dist/remote/server.d.ts
CHANGED
|
@@ -28,5 +28,5 @@ export declare function getLocalIPs(nets?: NodeJS.Dict<import("node:os").Network
|
|
|
28
28
|
* Tailscale line uses the MagicDNS host when known (resolves to the same node,
|
|
29
29
|
* but is what SSH and webpush certs need), falling back to the raw IP. */
|
|
30
30
|
export declare function formatAddresses(ips: LocalIPs, port: number, tsHost?: string): AddressLine[];
|
|
31
|
-
export declare function startServer(onMessage: MessageCallback, getHtml: (wsUrl: string) => string): Promise<ServerHandle>;
|
|
31
|
+
export declare function startServer(onMessage: MessageCallback, getHtml: (wsUrl: string) => string, onInterrupt?: () => void): Promise<ServerHandle>;
|
|
32
32
|
export {};
|
package/dist/remote/server.js
CHANGED
|
@@ -64,7 +64,7 @@ async function findPort(start, max) {
|
|
|
64
64
|
}
|
|
65
65
|
throw new Error(`No free port found in range ${start}–${start + max - 1}`);
|
|
66
66
|
}
|
|
67
|
-
export async function startServer(onMessage, getHtml) {
|
|
67
|
+
export async function startServer(onMessage, getHtml, onInterrupt) {
|
|
68
68
|
const port = await findPort(8800, 100);
|
|
69
69
|
const ips = getLocalIPs();
|
|
70
70
|
const ip = ips.primary;
|
|
@@ -159,6 +159,10 @@ export async function startServer(onMessage, getHtml) {
|
|
|
159
159
|
}
|
|
160
160
|
if (!isClientMessage(msg))
|
|
161
161
|
return;
|
|
162
|
+
if (msg.type === 'interrupt') {
|
|
163
|
+
onInterrupt?.();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
162
166
|
if (msg.type === 'prompt_answer') {
|
|
163
167
|
answerPrompt(msg.id, msg.value);
|
|
164
168
|
return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HistoryBuffer } from './history.js';
|
|
2
2
|
import type { Turn, Part } from './history.js';
|
|
3
|
-
import type { ContextUsage, PromptMessage } from './protocol.js';
|
|
3
|
+
import type { ContextUsage, PromptMessage, WidgetData } from './protocol.js';
|
|
4
4
|
export interface LiveTurn {
|
|
5
5
|
/** Ordered assistant content (text segments + tool calls) for this run. */
|
|
6
6
|
parts: Part[];
|
|
@@ -14,36 +14,50 @@ export interface SnapshotMessage {
|
|
|
14
14
|
live: LiveTurn | null;
|
|
15
15
|
agentRunning: boolean;
|
|
16
16
|
taskWidget: string[] | null;
|
|
17
|
+
taskWidgetData: WidgetData | null;
|
|
17
18
|
prompt: PromptMessage | null;
|
|
18
19
|
context: ContextUsage | null;
|
|
20
|
+
model: string | null;
|
|
19
21
|
}
|
|
20
22
|
interface SessionState {
|
|
21
23
|
history: HistoryBuffer;
|
|
22
24
|
live: LiveTurn | null;
|
|
23
25
|
agentRunning: boolean;
|
|
24
26
|
taskWidget: string[] | null;
|
|
27
|
+
taskWidgetData: WidgetData | null;
|
|
25
28
|
prompt: PromptMessage | null;
|
|
26
29
|
context: ContextUsage | null;
|
|
30
|
+
/** Human-readable active model name (e.g. "Qwen3.6 27B"), for the header chip. */
|
|
31
|
+
model: string | null;
|
|
32
|
+
/** tool start timestamps (ms), keyed by toolCallId — kept off the serialized
|
|
33
|
+
* parts so it never reaches the client; used only to compute elapsedMs. */
|
|
34
|
+
toolStarts: Record<string, number>;
|
|
27
35
|
/** Broadcast sink — swapped in tests via _setSink. */
|
|
28
36
|
sink: (msg: unknown) => void;
|
|
29
37
|
}
|
|
30
38
|
export declare function getState(): SessionState;
|
|
31
39
|
/** @internal Test-only: swap the broadcast sink to capture emitted deltas. */
|
|
32
40
|
export declare function _setSink(fn: (msg: unknown) => void): void;
|
|
33
|
-
export declare function agentStart(): void;
|
|
41
|
+
export declare function agentStart(model?: string): void;
|
|
34
42
|
export declare function appendText(delta: string): void;
|
|
35
43
|
export declare function textEnd(): void;
|
|
44
|
+
/** Accumulate a reasoning/thinking delta into the current thinking part (a new
|
|
45
|
+
* part opens when the previous one is closed or the last part isn't thinking).
|
|
46
|
+
* Stored as a part kind so a reconnect snapshot replays the collapsed block. */
|
|
47
|
+
export declare function appendThinking(delta: string): void;
|
|
48
|
+
export declare function thinkingEnd(): void;
|
|
36
49
|
export declare function startTool(toolCallId: string, toolName: string, args: unknown): void;
|
|
37
50
|
export declare function updateTool(toolCallId: string, partialResult: unknown): void;
|
|
38
51
|
export declare function endTool(toolCallId: string, toolName: string, result: unknown, isError: boolean): void;
|
|
39
|
-
export declare function agentEnd(context: ContextUsage): void;
|
|
52
|
+
export declare function agentEnd(context: ContextUsage, model?: string): void;
|
|
40
53
|
export declare function addUserTurn(text: string): void;
|
|
41
54
|
export declare function addError(message: string): void;
|
|
42
55
|
/** A persistent inline note (committed to the transcript so it survives reconnect)
|
|
43
56
|
* plus a live delta so connected clients render it immediately. */
|
|
44
57
|
export declare function addSystemNote(text: string): void;
|
|
45
|
-
/** The single task-widget slot. Empty/undefined lines clear it.
|
|
46
|
-
|
|
58
|
+
/** The single task-widget slot. Empty/undefined lines clear it. `data` is the
|
|
59
|
+
* structured view rendered by the browser; the lines remain the fallback. */
|
|
60
|
+
export declare function setTaskWidget(lines: string[] | null | undefined, data?: WidgetData | null): void;
|
|
47
61
|
export declare function setPrompt(prompt: PromptMessage): void;
|
|
48
62
|
export declare function clearPrompt(id: string): void;
|
|
49
63
|
export declare function setContext(context: ContextUsage): void;
|
|
@@ -18,8 +18,11 @@ function fresh() {
|
|
|
18
18
|
live: null,
|
|
19
19
|
agentRunning: false,
|
|
20
20
|
taskWidget: null,
|
|
21
|
+
taskWidgetData: null,
|
|
21
22
|
prompt: null,
|
|
22
23
|
context: null,
|
|
24
|
+
model: null,
|
|
25
|
+
toolStarts: {},
|
|
23
26
|
sink: wsBroadcast
|
|
24
27
|
};
|
|
25
28
|
}
|
|
@@ -38,11 +41,13 @@ function ensureLive(s) {
|
|
|
38
41
|
return s.live;
|
|
39
42
|
}
|
|
40
43
|
// ─── Mutators ────────────────────────────────────────────────────────────────
|
|
41
|
-
export function agentStart() {
|
|
44
|
+
export function agentStart(model) {
|
|
42
45
|
const s = getState();
|
|
43
46
|
s.live = { parts: [], textOpen: false };
|
|
44
47
|
s.agentRunning = true;
|
|
45
|
-
|
|
48
|
+
if (model)
|
|
49
|
+
s.model = model;
|
|
50
|
+
s.sink({ type: 'agent_start', model: s.model });
|
|
46
51
|
}
|
|
47
52
|
export function appendText(delta) {
|
|
48
53
|
const s = getState();
|
|
@@ -63,6 +68,29 @@ export function textEnd() {
|
|
|
63
68
|
s.live.textOpen = false; // next text starts a new segment
|
|
64
69
|
s.sink({ type: 'text_end' });
|
|
65
70
|
}
|
|
71
|
+
/** Accumulate a reasoning/thinking delta into the current thinking part (a new
|
|
72
|
+
* part opens when the previous one is closed or the last part isn't thinking).
|
|
73
|
+
* Stored as a part kind so a reconnect snapshot replays the collapsed block. */
|
|
74
|
+
export function appendThinking(delta) {
|
|
75
|
+
const s = getState();
|
|
76
|
+
const live = ensureLive(s);
|
|
77
|
+
const last = live.parts[live.parts.length - 1];
|
|
78
|
+
if (last && last.kind === 'thinking' && !last.done) {
|
|
79
|
+
last.text += delta;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
live.parts.push({ kind: 'thinking', text: delta, done: false });
|
|
83
|
+
}
|
|
84
|
+
live.textOpen = false; // a following text delta begins a fresh bubble
|
|
85
|
+
s.sink({ type: 'thinking_delta', delta });
|
|
86
|
+
}
|
|
87
|
+
export function thinkingEnd() {
|
|
88
|
+
const s = getState();
|
|
89
|
+
const last = s.live?.parts[s.live.parts.length - 1];
|
|
90
|
+
if (last && last.kind === 'thinking')
|
|
91
|
+
last.done = true;
|
|
92
|
+
s.sink({ type: 'thinking_end' });
|
|
93
|
+
}
|
|
66
94
|
export function startTool(toolCallId, toolName, args) {
|
|
67
95
|
const s = getState();
|
|
68
96
|
const live = ensureLive(s);
|
|
@@ -76,6 +104,7 @@ export function startTool(toolCallId, toolName, args) {
|
|
|
76
104
|
isError: false,
|
|
77
105
|
done: false
|
|
78
106
|
});
|
|
107
|
+
s.toolStarts[toolCallId] = Date.now();
|
|
79
108
|
s.sink({ type: 'tool_start', toolCallId, toolName, args });
|
|
80
109
|
}
|
|
81
110
|
export function updateTool(toolCallId, partialResult) {
|
|
@@ -85,10 +114,18 @@ export function endTool(toolCallId, toolName, result, isError) {
|
|
|
85
114
|
const s = getState();
|
|
86
115
|
const live = ensureLive(s);
|
|
87
116
|
const part = live.parts.find((p) => p.kind === 'tool' && p.toolCallId === toolCallId);
|
|
117
|
+
const startedAt = s.toolStarts[toolCallId];
|
|
118
|
+
let elapsedMs;
|
|
119
|
+
if (startedAt != null) {
|
|
120
|
+
elapsedMs = Date.now() - startedAt;
|
|
121
|
+
delete s.toolStarts[toolCallId];
|
|
122
|
+
}
|
|
88
123
|
if (part) {
|
|
89
124
|
part.result = result;
|
|
90
125
|
part.isError = isError;
|
|
91
126
|
part.done = true;
|
|
127
|
+
if (elapsedMs != null)
|
|
128
|
+
part.elapsedMs = elapsedMs;
|
|
92
129
|
}
|
|
93
130
|
else {
|
|
94
131
|
// tool_end without a matching start (shouldn't happen) — append a done tool.
|
|
@@ -102,16 +139,18 @@ export function endTool(toolCallId, toolName, result, isError) {
|
|
|
102
139
|
done: true
|
|
103
140
|
});
|
|
104
141
|
}
|
|
105
|
-
s.sink({ type: 'tool_end', toolCallId, toolName, result, isError });
|
|
142
|
+
s.sink({ type: 'tool_end', toolCallId, toolName, result, isError, elapsedMs });
|
|
106
143
|
}
|
|
107
|
-
export function agentEnd(context) {
|
|
144
|
+
export function agentEnd(context, model) {
|
|
108
145
|
const s = getState();
|
|
109
146
|
if (s.live)
|
|
110
147
|
s.history.addAssistantTurn(s.live.parts);
|
|
111
148
|
s.live = null;
|
|
112
149
|
s.agentRunning = false;
|
|
113
150
|
s.context = context;
|
|
114
|
-
|
|
151
|
+
if (model)
|
|
152
|
+
s.model = model;
|
|
153
|
+
s.sink({ type: 'agent_end', contextUsage: context, model: s.model });
|
|
115
154
|
}
|
|
116
155
|
export function addUserTurn(text) {
|
|
117
156
|
const s = getState();
|
|
@@ -132,11 +171,13 @@ export function addSystemNote(text) {
|
|
|
132
171
|
s.history.addSystemNote(text);
|
|
133
172
|
s.sink({ type: 'system_note', text });
|
|
134
173
|
}
|
|
135
|
-
/** The single task-widget slot. Empty/undefined lines clear it.
|
|
136
|
-
|
|
174
|
+
/** The single task-widget slot. Empty/undefined lines clear it. `data` is the
|
|
175
|
+
* structured view rendered by the browser; the lines remain the fallback. */
|
|
176
|
+
export function setTaskWidget(lines, data) {
|
|
137
177
|
const s = getState();
|
|
138
178
|
s.taskWidget = lines && lines.length ? lines : null;
|
|
139
|
-
s.
|
|
179
|
+
s.taskWidgetData = s.taskWidget ? (data ?? null) : null;
|
|
180
|
+
s.sink({ type: 'widget', lines: s.taskWidget, data: s.taskWidgetData });
|
|
140
181
|
}
|
|
141
182
|
export function setPrompt(prompt) {
|
|
142
183
|
const s = getState();
|
|
@@ -160,8 +201,11 @@ export function reset() {
|
|
|
160
201
|
s.live = null;
|
|
161
202
|
s.agentRunning = false;
|
|
162
203
|
s.taskWidget = null;
|
|
204
|
+
s.taskWidgetData = null;
|
|
163
205
|
s.prompt = null;
|
|
164
206
|
s.context = null;
|
|
207
|
+
// Deliberately keep s.model: the active model doesn't change across a /new,
|
|
208
|
+
// so the header chip needn't blank and re-fill on every session switch.
|
|
165
209
|
s.sink({ type: 'reset' });
|
|
166
210
|
}
|
|
167
211
|
/** Serialize the whole state for a (re)connecting client. */
|
|
@@ -173,7 +217,9 @@ export function snapshot() {
|
|
|
173
217
|
live: s.live ? { parts: [...s.live.parts], textOpen: s.live.textOpen } : null,
|
|
174
218
|
agentRunning: s.agentRunning,
|
|
175
219
|
taskWidget: s.taskWidget,
|
|
220
|
+
taskWidgetData: s.taskWidgetData,
|
|
176
221
|
prompt: s.prompt,
|
|
177
|
-
context: s.context
|
|
222
|
+
context: s.context,
|
|
223
|
+
model: s.model
|
|
178
224
|
};
|
|
179
225
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Generalized single-pass syntax highlighter, shipped as a JS string and
|
|
2
|
+
// concatenated into the page by ui.ts. It grew out of the client's original
|
|
3
|
+
// JS-only tokenizer: the string/comment/number/identifier logic is now shared and
|
|
4
|
+
// parameterized by a per-language config (keyword set + comment markers + string
|
|
5
|
+
// flavors). Unknown languages fall back to escaped plain text (unchanged).
|
|
6
|
+
//
|
|
7
|
+
// Depends on escHtml (ui-render.ts) — both land in the same <script>.
|
|
8
|
+
/** Emitted client JS (top-level function declarations + language tables). */
|
|
9
|
+
export function highlightModule() {
|
|
10
|
+
return ` var HL_JS_KW = new Set(('break case catch class const continue debugger default delete do else export '
|
|
11
|
+
+ 'extends finally for from function if import in instanceof let new of return static super switch this '
|
|
12
|
+
+ 'throw try typeof var void while with yield async await type interface enum implements abstract as declare '
|
|
13
|
+
+ 'namespace readonly undefined null true false override satisfies public private protected get set').split(' '));
|
|
14
|
+
var HL_PY_KW = new Set(('def return if elif else for while in not and or import from as class try except finally '
|
|
15
|
+
+ 'raise with lambda yield pass break continue global nonlocal assert del None True False async await is print self').split(' '));
|
|
16
|
+
var HL_SH_KW = new Set(('if then else elif fi for while do done case esac in function return echo export local read '
|
|
17
|
+
+ 'cd set unset source alias exit test').split(' '));
|
|
18
|
+
var HL_SQL_KW = new Set(('select from where insert into values update set delete create table drop alter add column '
|
|
19
|
+
+ 'join left right inner outer full on group by order having limit offset union and or not null is as distinct '
|
|
20
|
+
+ 'count sum avg min max primary key foreign references index unique default int integer text varchar char boolean '
|
|
21
|
+
+ 'timestamp date time serial begin commit rollback').split(' '));
|
|
22
|
+
var HL_GO_KW = new Set(('func package import var const type struct interface map chan go defer return if else for range '
|
|
23
|
+
+ 'switch case default break continue select nil true false make new append len cap string int int64 float64 bool byte rune error').split(' '));
|
|
24
|
+
var HL_RUST_KW = new Set(('fn let mut const static struct enum impl trait pub use mod match if else for while loop return '
|
|
25
|
+
+ 'break continue self Self super crate as where move ref dyn async await unsafe true false None Some Ok Err String Vec Option Result Box').split(' '));
|
|
26
|
+
var HL_YAML_KW = new Set('true false null yes no on off'.split(' '));
|
|
27
|
+
var HL_JSON_KW = new Set('true false null'.split(' '));
|
|
28
|
+
|
|
29
|
+
var HL_LANGS = {
|
|
30
|
+
js: {kw: HL_JS_KW, line: '//', block: ['/*', '*/'], tmpl: true},
|
|
31
|
+
ts: {kw: HL_JS_KW, line: '//', block: ['/*', '*/'], tmpl: true},
|
|
32
|
+
python: {kw: HL_PY_KW, line: '#', block: null, triple: true},
|
|
33
|
+
sh: {kw: HL_SH_KW, line: '#', block: null},
|
|
34
|
+
sql: {kw: HL_SQL_KW, line: '--', block: ['/*', '*/'], ci: true},
|
|
35
|
+
go: {kw: HL_GO_KW, line: '//', block: ['/*', '*/'], tmpl: true},
|
|
36
|
+
rust: {kw: HL_RUST_KW, line: '//', block: ['/*', '*/']},
|
|
37
|
+
yaml: {kw: HL_YAML_KW, line: '#', block: null},
|
|
38
|
+
json: {kw: HL_JSON_KW, line: null, block: null},
|
|
39
|
+
css: {kw: null, line: null, block: ['/*', '*/']}
|
|
40
|
+
};
|
|
41
|
+
var HL_ALIAS = {javascript: 'js', jsx: 'js', mjs: 'js', cjs: 'js', node: 'js', typescript: 'ts', tsx: 'ts',
|
|
42
|
+
py: 'python', python3: 'python', bash: 'sh', shell: 'sh', shellscript: 'sh', zsh: 'sh', console: 'sh',
|
|
43
|
+
yml: 'yaml', rs: 'rust', golang: 'go', htm: 'html', xml: 'html', svg: 'html', scss: 'css'};
|
|
44
|
+
|
|
45
|
+
function hlSpan(cls, text) { return '<span class="' + cls + '">' + escHtml(text) + '</span>'; }
|
|
46
|
+
|
|
47
|
+
function tokenize(code, cfg) {
|
|
48
|
+
var BT = String.fromCharCode(96);
|
|
49
|
+
var r = '', i = 0, n = code.length;
|
|
50
|
+
while (i < n) {
|
|
51
|
+
var ch = code[i];
|
|
52
|
+
if (cfg.block && code.startsWith(cfg.block[0], i)) {
|
|
53
|
+
var end = code.indexOf(cfg.block[1], i + cfg.block[0].length);
|
|
54
|
+
var je = end < 0 ? n : end + cfg.block[1].length;
|
|
55
|
+
r += hlSpan('hl-cmt', code.slice(i, je)); i = je; continue;
|
|
56
|
+
}
|
|
57
|
+
if (cfg.line && code.startsWith(cfg.line, i)) {
|
|
58
|
+
var jl = i; while (jl < n && code[jl] !== '\\n') jl++;
|
|
59
|
+
r += hlSpan('hl-cmt', code.slice(i, jl)); i = jl; continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === '"' || ch === "'" || (cfg.tmpl && ch === BT)) {
|
|
62
|
+
if (cfg.triple && code[i + 1] === ch && code[i + 2] === ch) {
|
|
63
|
+
var close = code.indexOf(ch + ch + ch, i + 3);
|
|
64
|
+
var jt = close < 0 ? n : close + 3;
|
|
65
|
+
r += hlSpan('hl-str', code.slice(i, jt)); i = jt; continue;
|
|
66
|
+
}
|
|
67
|
+
var js = i + 1;
|
|
68
|
+
while (js < n) { if (code[js] === '\\\\') { js += 2; continue; } if (code[js] === ch || code[js] === '\\n') break; js++; }
|
|
69
|
+
if (code[js] === ch) js++;
|
|
70
|
+
r += hlSpan('hl-str', code.slice(i, js)); i = js; continue;
|
|
71
|
+
}
|
|
72
|
+
if (ch >= '0' && ch <= '9') {
|
|
73
|
+
var jn = i;
|
|
74
|
+
if (code[i] === '0' && /[xXoObB]/.test(code[i + 1] || '')) {
|
|
75
|
+
jn += 2; while (jn < n && /[0-9a-fA-F_]/.test(code[jn])) jn++;
|
|
76
|
+
} else {
|
|
77
|
+
while (jn < n && ((code[jn] >= '0' && code[jn] <= '9') || code[jn] === '_')) jn++;
|
|
78
|
+
if (code[jn] === '.') { jn++; while (jn < n && code[jn] >= '0' && code[jn] <= '9') jn++; }
|
|
79
|
+
if (code[jn] === 'e' || code[jn] === 'E') { jn++; if (code[jn] === '+' || code[jn] === '-') jn++; while (jn < n && code[jn] >= '0' && code[jn] <= '9') jn++; }
|
|
80
|
+
if (code[jn] === 'n') jn++;
|
|
81
|
+
}
|
|
82
|
+
r += hlSpan('hl-num', code.slice(i, jn)); i = jn; continue;
|
|
83
|
+
}
|
|
84
|
+
if (/[a-zA-Z_$]/.test(ch)) {
|
|
85
|
+
var jw = i; while (jw < n && /[a-zA-Z0-9_$]/.test(code[jw])) jw++;
|
|
86
|
+
var word = code.slice(i, jw);
|
|
87
|
+
var kwHit = cfg.kw && (cfg.kw.has(word) || (cfg.ci && cfg.kw.has(word.toLowerCase())));
|
|
88
|
+
if (kwHit) r += hlSpan('hl-kw', word);
|
|
89
|
+
else if (code[jw] === '(') r += hlSpan('hl-fn', word);
|
|
90
|
+
else r += escHtml(word);
|
|
91
|
+
i = jw; continue;
|
|
92
|
+
}
|
|
93
|
+
r += escHtml(ch); i++;
|
|
94
|
+
}
|
|
95
|
+
return r;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Markup languages don't fit the identifier tokenizer; highlight tag names and
|
|
99
|
+
// attribute names off the already-escaped text.
|
|
100
|
+
function highlightMarkup(code) {
|
|
101
|
+
var e = escHtml(code);
|
|
102
|
+
// Attribute names first (before any spans exist), then tag names — otherwise
|
|
103
|
+
// the attribute pass would re-match the class="" inside the spans it inserted.
|
|
104
|
+
e = e.replace(/([a-zA-Z-]+)=("|")/g, function (_, a, q) { return '<span class="hl-fn">' + a + '</span>=' + q; });
|
|
105
|
+
e = e.replace(/(<\\/?)([a-zA-Z][\\w-]*)/g, function (_, p, t) { return p + '<span class="hl-kw">' + t + '</span>'; });
|
|
106
|
+
return e;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function syntaxHighlight(code, lang) {
|
|
110
|
+
code = String(code == null ? '' : code);
|
|
111
|
+
lang = (lang || '').toLowerCase();
|
|
112
|
+
lang = HL_ALIAS[lang] || lang;
|
|
113
|
+
if (lang === 'html') return highlightMarkup(code);
|
|
114
|
+
var cfg = HL_LANGS[lang];
|
|
115
|
+
if (!cfg) return escHtml(code);
|
|
116
|
+
return tokenize(code, cfg);
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
}
|