@mjasnikovs/pi-task 0.17.22 → 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 +9 -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 +16 -5
- package/dist/remote/session-state.js +44 -8
- package/dist/remote/ui-render.js +6 -2
- package/dist/remote/ui-script.js +373 -23
- package/dist/remote/ui-styles.d.ts +1 -1
- package/dist/remote/ui-styles.js +116 -3
- package/dist/remote/ui.js +13 -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;
|
|
@@ -14,7 +20,7 @@ export interface ToolPart {
|
|
|
14
20
|
/** Wall-clock duration once finished, shown dimly in the tool summary. */
|
|
15
21
|
elapsedMs?: number;
|
|
16
22
|
}
|
|
17
|
-
export type Part = TextPart | ToolPart;
|
|
23
|
+
export type Part = TextPart | ThinkingPart | ToolPart;
|
|
18
24
|
export interface Turn {
|
|
19
25
|
role: 'user' | 'assistant' | 'system';
|
|
20
26
|
/** User text, error text, or a system note. Assistant content lives in `parts`. */
|
|
@@ -22,6 +28,8 @@ export interface Turn {
|
|
|
22
28
|
/** Ordered assistant content (text + tools). */
|
|
23
29
|
parts?: Part[];
|
|
24
30
|
error?: boolean;
|
|
31
|
+
/** Epoch ms when the turn was committed — the client renders a dim HH:MM. */
|
|
32
|
+
ts?: number;
|
|
25
33
|
}
|
|
26
34
|
export declare class HistoryBuffer {
|
|
27
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,16 +14,21 @@ 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;
|
|
27
32
|
/** tool start timestamps (ms), keyed by toolCallId — kept off the serialized
|
|
28
33
|
* parts so it never reaches the client; used only to compute elapsedMs. */
|
|
29
34
|
toolStarts: Record<string, number>;
|
|
@@ -33,20 +38,26 @@ interface SessionState {
|
|
|
33
38
|
export declare function getState(): SessionState;
|
|
34
39
|
/** @internal Test-only: swap the broadcast sink to capture emitted deltas. */
|
|
35
40
|
export declare function _setSink(fn: (msg: unknown) => void): void;
|
|
36
|
-
export declare function agentStart(): void;
|
|
41
|
+
export declare function agentStart(model?: string): void;
|
|
37
42
|
export declare function appendText(delta: string): void;
|
|
38
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;
|
|
39
49
|
export declare function startTool(toolCallId: string, toolName: string, args: unknown): void;
|
|
40
50
|
export declare function updateTool(toolCallId: string, partialResult: unknown): void;
|
|
41
51
|
export declare function endTool(toolCallId: string, toolName: string, result: unknown, isError: boolean): void;
|
|
42
|
-
export declare function agentEnd(context: ContextUsage): void;
|
|
52
|
+
export declare function agentEnd(context: ContextUsage, model?: string): void;
|
|
43
53
|
export declare function addUserTurn(text: string): void;
|
|
44
54
|
export declare function addError(message: string): void;
|
|
45
55
|
/** A persistent inline note (committed to the transcript so it survives reconnect)
|
|
46
56
|
* plus a live delta so connected clients render it immediately. */
|
|
47
57
|
export declare function addSystemNote(text: string): void;
|
|
48
|
-
/** The single task-widget slot. Empty/undefined lines clear it.
|
|
49
|
-
|
|
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;
|
|
50
61
|
export declare function setPrompt(prompt: PromptMessage): void;
|
|
51
62
|
export declare function clearPrompt(id: string): void;
|
|
52
63
|
export declare function setContext(context: ContextUsage): void;
|
|
@@ -18,8 +18,10 @@ 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,
|
|
23
25
|
toolStarts: {},
|
|
24
26
|
sink: wsBroadcast
|
|
25
27
|
};
|
|
@@ -39,11 +41,13 @@ function ensureLive(s) {
|
|
|
39
41
|
return s.live;
|
|
40
42
|
}
|
|
41
43
|
// ─── Mutators ────────────────────────────────────────────────────────────────
|
|
42
|
-
export function agentStart() {
|
|
44
|
+
export function agentStart(model) {
|
|
43
45
|
const s = getState();
|
|
44
46
|
s.live = { parts: [], textOpen: false };
|
|
45
47
|
s.agentRunning = true;
|
|
46
|
-
|
|
48
|
+
if (model)
|
|
49
|
+
s.model = model;
|
|
50
|
+
s.sink({ type: 'agent_start', model: s.model });
|
|
47
51
|
}
|
|
48
52
|
export function appendText(delta) {
|
|
49
53
|
const s = getState();
|
|
@@ -64,6 +68,29 @@ export function textEnd() {
|
|
|
64
68
|
s.live.textOpen = false; // next text starts a new segment
|
|
65
69
|
s.sink({ type: 'text_end' });
|
|
66
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
|
+
}
|
|
67
94
|
export function startTool(toolCallId, toolName, args) {
|
|
68
95
|
const s = getState();
|
|
69
96
|
const live = ensureLive(s);
|
|
@@ -114,14 +141,16 @@ export function endTool(toolCallId, toolName, result, isError) {
|
|
|
114
141
|
}
|
|
115
142
|
s.sink({ type: 'tool_end', toolCallId, toolName, result, isError, elapsedMs });
|
|
116
143
|
}
|
|
117
|
-
export function agentEnd(context) {
|
|
144
|
+
export function agentEnd(context, model) {
|
|
118
145
|
const s = getState();
|
|
119
146
|
if (s.live)
|
|
120
147
|
s.history.addAssistantTurn(s.live.parts);
|
|
121
148
|
s.live = null;
|
|
122
149
|
s.agentRunning = false;
|
|
123
150
|
s.context = context;
|
|
124
|
-
|
|
151
|
+
if (model)
|
|
152
|
+
s.model = model;
|
|
153
|
+
s.sink({ type: 'agent_end', contextUsage: context, model: s.model });
|
|
125
154
|
}
|
|
126
155
|
export function addUserTurn(text) {
|
|
127
156
|
const s = getState();
|
|
@@ -142,11 +171,13 @@ export function addSystemNote(text) {
|
|
|
142
171
|
s.history.addSystemNote(text);
|
|
143
172
|
s.sink({ type: 'system_note', text });
|
|
144
173
|
}
|
|
145
|
-
/** The single task-widget slot. Empty/undefined lines clear it.
|
|
146
|
-
|
|
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) {
|
|
147
177
|
const s = getState();
|
|
148
178
|
s.taskWidget = lines && lines.length ? lines : null;
|
|
149
|
-
s.
|
|
179
|
+
s.taskWidgetData = s.taskWidget ? (data ?? null) : null;
|
|
180
|
+
s.sink({ type: 'widget', lines: s.taskWidget, data: s.taskWidgetData });
|
|
150
181
|
}
|
|
151
182
|
export function setPrompt(prompt) {
|
|
152
183
|
const s = getState();
|
|
@@ -170,8 +201,11 @@ export function reset() {
|
|
|
170
201
|
s.live = null;
|
|
171
202
|
s.agentRunning = false;
|
|
172
203
|
s.taskWidget = null;
|
|
204
|
+
s.taskWidgetData = null;
|
|
173
205
|
s.prompt = null;
|
|
174
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.
|
|
175
209
|
s.sink({ type: 'reset' });
|
|
176
210
|
}
|
|
177
211
|
/** Serialize the whole state for a (re)connecting client. */
|
|
@@ -183,7 +217,9 @@ export function snapshot() {
|
|
|
183
217
|
live: s.live ? { parts: [...s.live.parts], textOpen: s.live.textOpen } : null,
|
|
184
218
|
agentRunning: s.agentRunning,
|
|
185
219
|
taskWidget: s.taskWidget,
|
|
220
|
+
taskWidgetData: s.taskWidgetData,
|
|
186
221
|
prompt: s.prompt,
|
|
187
|
-
context: s.context
|
|
222
|
+
context: s.context,
|
|
223
|
+
model: s.model
|
|
188
224
|
};
|
|
189
225
|
}
|
package/dist/remote/ui-render.js
CHANGED
|
@@ -156,8 +156,12 @@ export function renderModule() {
|
|
|
156
156
|
for (var k = 0; k < blocks.length; k++) {
|
|
157
157
|
var b = blocks[k];
|
|
158
158
|
if (b.type === 'code') {
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
// Header row carries the language label and a copy button (wired by
|
|
160
|
+
// event delegation in clientScript — it reads the <code> textContent).
|
|
161
|
+
out += '<div class="code-block"><div class="code-head">'
|
|
162
|
+
+ '<div class="code-lang">' + (b.lang ? escHtml(b.lang) : '') + '</div>'
|
|
163
|
+
+ '<button class="copy-btn" type="button" aria-label="Copy code">Copy</button>'
|
|
164
|
+
+ '</div>'
|
|
161
165
|
+ '<pre><code>' + syntaxHighlight(b.content, b.lang) + '</code></pre></div>';
|
|
162
166
|
} else {
|
|
163
167
|
out += renderProse(b.content);
|