@mjasnikovs/pi-task 0.39.2 → 0.39.3
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/config/register.js +8 -2
- package/dist/remote/bridge.d.ts +65 -1
- package/dist/remote/bridge.js +142 -22
- package/dist/remote/protocol.d.ts +9 -6
- package/dist/remote/register.js +13 -7
- package/dist/remote/session-state.js +6 -1
- package/dist/remote/ui-script.js +13 -10
- package/dist/remote/ui-styles.d.ts +1 -1
- package/dist/remote/ui-styles.js +3 -4
- package/dist/remote/ui.js +0 -1
- package/dist/task/auto-orchestrator.js +21 -15
- package/dist/task/failure-classifier.js +4 -5
- package/dist/task/gate-child.js +3 -2
- package/dist/task/orchestrator.js +18 -15
- package/dist/task/phases.js +2 -2
- package/dist/task/plan-orchestrator.js +14 -14
- package/dist/task/run-final-gate.js +11 -11
- package/dist/task/task-gates.js +22 -16
- package/dist/task/widget.js +30 -39
- package/package.json +1 -1
package/dist/config/register.js
CHANGED
|
@@ -3,7 +3,7 @@ import { effectiveSetting, offeredLevels } from '../shared/reasoning-capability.
|
|
|
3
3
|
import { resolveModel, specOf } from '../shared/model-resolve.js';
|
|
4
4
|
import { MODEL_INHERIT } from './group-models.js';
|
|
5
5
|
import { PairPicker } from './option-picker.js';
|
|
6
|
-
import { registerBridgeCommand } from '../remote/bridge.js';
|
|
6
|
+
import { registerBridgeCommand, isRemoteOrigin, publishNote } from '../remote/bridge.js';
|
|
7
7
|
import { readPkgVersion } from '../shared/pkg-version.js';
|
|
8
8
|
import { SEARCH_PROVIDERS, SEARCH_PROVIDER_LABELS, providerForLabel } from '../workers/search-types.js';
|
|
9
9
|
import { COMMAND_TIMEOUT_OPTIONS, DEBUG_LOG_OPTIONS, getConfig, sanitizeDebugLogs, saveConfig, STREAM_INACTIVITY_OPTIONS } from './config.js';
|
|
@@ -836,7 +836,10 @@ async function handleTaskConfig(_args, ctx, getTools = () => []) {
|
|
|
836
836
|
// it can only be read here, when the menu opens, never at registration.
|
|
837
837
|
const tools = getTools();
|
|
838
838
|
const catalog = liveCatalog(ctx);
|
|
839
|
-
|
|
839
|
+
// The panel is a TUI component. A browser caller has no way to draw it and no
|
|
840
|
+
// way to close it, so awaiting it there parks the remote on the host's
|
|
841
|
+
// terminal — the failure issue #1 fixed for /task-plan.
|
|
842
|
+
if (ctx.mode !== 'tui' || isRemoteOrigin(ctx)) {
|
|
840
843
|
// Built from panelItems and reading the SAME `format` the panel does,
|
|
841
844
|
// so the two renderings cannot disagree about what a setting says. A
|
|
842
845
|
// second walk of the same tables is the one place nobody would notice
|
|
@@ -850,6 +853,9 @@ async function handleTaskConfig(_args, ctx, getTools = () => []) {
|
|
|
850
853
|
`[${i.label.trim()}]`
|
|
851
854
|
: `${(i.headlessLabel ?? i.label).padEnd(22)} ${i.currentValue}`);
|
|
852
855
|
ctx.ui.notify(lines.join(' | '), 'info');
|
|
856
|
+
// One row per line: the `|`-joined form is for a terminal that has one
|
|
857
|
+
// line to spare, and the browser has a whole transcript.
|
|
858
|
+
publishNote(lines.join('\n'));
|
|
853
859
|
return;
|
|
854
860
|
}
|
|
855
861
|
// Built ONCE and shared by the renderer, the dispatch and the refresh, so
|
package/dist/remote/bridge.d.ts
CHANGED
|
@@ -17,6 +17,17 @@ export declare function getBridge(): BridgeState;
|
|
|
17
17
|
/** Resolve the remote side of a pending prompt. First call wins; later calls
|
|
18
18
|
* (duplicate frames, a second browser, local-after-remote) are ignored. */
|
|
19
19
|
export declare function answerPrompt(id: string, value: string | undefined): void;
|
|
20
|
+
/**
|
|
21
|
+
* Settle every parked ask with undefined. Its one caller is the remote `/new`,
|
|
22
|
+
* whose reset wipes the prompt slot and tells the browser to close its card,
|
|
23
|
+
* leaving the resolver here with no surface on any device — and, on a host with
|
|
24
|
+
* no TUI, no local half either.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately NOT on every `session_start`: that also fires for the /task
|
|
27
|
+
* handoff's own newSession, where cancelling would read to the planner as the
|
|
28
|
+
* user skipping a question they were never shown.
|
|
29
|
+
*/
|
|
30
|
+
export declare function cancelPendingPrompts(): void;
|
|
20
31
|
export interface AskSpec {
|
|
21
32
|
/** Themed, possibly multi-line title for the local TUI input. */
|
|
22
33
|
localTitle: string;
|
|
@@ -86,6 +97,17 @@ export interface AskSpec {
|
|
|
86
97
|
value: string;
|
|
87
98
|
}[];
|
|
88
99
|
}
|
|
100
|
+
/** A read-only display fanned out to both surfaces by {@link SessionUI.show}. */
|
|
101
|
+
export interface ShowSpec {
|
|
102
|
+
/** Title for the local terminal editor. */
|
|
103
|
+
localTitle: string;
|
|
104
|
+
/** Full text the local editor opens with. */
|
|
105
|
+
localText: string;
|
|
106
|
+
/** Card header on the browser. Plain text. */
|
|
107
|
+
question: string;
|
|
108
|
+
/** Card body on the browser, markdown-rendered in the panel. */
|
|
109
|
+
body: string;
|
|
110
|
+
}
|
|
89
111
|
/** Wraps a live command ctx and fans interactions out to local TUI + browsers. */
|
|
90
112
|
export declare class SessionUI {
|
|
91
113
|
private readonly ctx;
|
|
@@ -95,6 +117,22 @@ export declare class SessionUI {
|
|
|
95
117
|
get hasUI(): boolean;
|
|
96
118
|
/** Race the local input against a remote answer; first to settle wins. */
|
|
97
119
|
ask(spec: AskSpec): Promise<string | undefined>;
|
|
120
|
+
/**
|
|
121
|
+
* Show text that only needs dismissing. Locally that is the terminal editor;
|
|
122
|
+
* remotely it is a dismiss-only prompt card, and it has to be a prompt rather
|
|
123
|
+
* than a `viewer` because only the prompt slot is in the reconnect snapshot
|
|
124
|
+
* and only a prompt can settle the caller from the browser.
|
|
125
|
+
*
|
|
126
|
+
* pi's `ui.editor` takes no AbortSignal, so a remote dismissal cannot close
|
|
127
|
+
* the terminal editor the way a remote answer aborts a local ask. It is left
|
|
128
|
+
* open and whatever it eventually returns is discarded — the run has moved on.
|
|
129
|
+
*/
|
|
130
|
+
show(spec: ShowSpec): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* The shared half of ask() and show(): publish one prompt card, open the
|
|
133
|
+
* local dialog beside it, and take whichever settles first.
|
|
134
|
+
*/
|
|
135
|
+
private race;
|
|
98
136
|
/**
|
|
99
137
|
* The local-TUI half of ask(). With `spec.options` it renders the boxed
|
|
100
138
|
* picker (each answer in its own bounding box, the first/recommended one
|
|
@@ -126,7 +164,33 @@ export declare function interruptAgent(): void;
|
|
|
126
164
|
* transient toast since they don't need to linger.
|
|
127
165
|
*/
|
|
128
166
|
export declare function publishLifecycleNotice(message: string, level: 'info' | 'warning' | 'error'): void;
|
|
129
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Say it on BOTH surfaces. The local notify is attempted first and its failure
|
|
169
|
+
* absorbed — a stale ctx must not cost the remote the message, which is the one
|
|
170
|
+
* surface still there when nobody is at the terminal.
|
|
171
|
+
*/
|
|
172
|
+
export declare function notifyBoth(ctx: Pick<ExtensionCommandContext, 'ui'>, message: string, level: 'info' | 'warning' | 'error'): void;
|
|
173
|
+
/** {@link notifyBoth} for something that must outlive a toast — see
|
|
174
|
+
* {@link publishRunNotice}. */
|
|
175
|
+
export declare function notifyRun(ctx: Pick<ExtensionCommandContext, 'ui'>, message: string, level: 'info' | 'warning' | 'error'): void;
|
|
176
|
+
/**
|
|
177
|
+
* A run outcome the remote must still have later: what a gate decided, what a
|
|
178
|
+
* commit failed to do, what a resume refused. Unlike publishLifecycleNotice it
|
|
179
|
+
* persists at EVERY level, because "still there in the morning" is the reason
|
|
180
|
+
* these are published at all — a warning that vanishes after four seconds tells
|
|
181
|
+
* an absent user nothing.
|
|
182
|
+
*/
|
|
183
|
+
export declare function publishRunNotice(message: string, level: 'info' | 'warning' | 'error'): void;
|
|
184
|
+
/** Commit text to the remote transcript. Unlike a notify it is in the reconnect
|
|
185
|
+
* snapshot, so it is still there after a refresh. */
|
|
186
|
+
export declare function publishNote(text: string): void;
|
|
187
|
+
/**
|
|
188
|
+
* True when this ctx reached the handler through the browser rather than the
|
|
189
|
+
* terminal. A command that can only draw itself on the host TUI must check it:
|
|
190
|
+
* awaiting a local overlay for a remote caller parks the browser on a dialog
|
|
191
|
+
* nobody is sitting in front of.
|
|
192
|
+
*/
|
|
193
|
+
export declare function isRemoteOrigin(ctx: ExtensionCommandContext): boolean;
|
|
130
194
|
/**
|
|
131
195
|
* Wraps an event-scoped ExtensionContext so it can be used as a command ctx
|
|
132
196
|
* for commands that don't need newSession (e.g. /task-resume, /task-list,
|
package/dist/remote/bridge.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { broadcast as wsBroadcast } from './broadcast.js';
|
|
2
2
|
import { pushNotify } from './push.js';
|
|
3
|
-
import { setPrompt, clearPrompt, addError } from './session-state.js';
|
|
3
|
+
import { setPrompt, clearPrompt, addError, addSystemNote, getState } from './session-state.js';
|
|
4
4
|
import { askQuestionBox } from '../task/question-box.js';
|
|
5
5
|
const g = globalThis;
|
|
6
6
|
export function getBridge() {
|
|
@@ -26,6 +26,22 @@ export function answerPrompt(id, value) {
|
|
|
26
26
|
b.pending.delete(id);
|
|
27
27
|
settle(value);
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Settle every parked ask with undefined. Its one caller is the remote `/new`,
|
|
31
|
+
* whose reset wipes the prompt slot and tells the browser to close its card,
|
|
32
|
+
* leaving the resolver here with no surface on any device — and, on a host with
|
|
33
|
+
* no TUI, no local half either.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately NOT on every `session_start`: that also fires for the /task
|
|
36
|
+
* handoff's own newSession, where cancelling would read to the planner as the
|
|
37
|
+
* user skipping a question they were never shown.
|
|
38
|
+
*/
|
|
39
|
+
export function cancelPendingPrompts() {
|
|
40
|
+
const b = getBridge();
|
|
41
|
+
for (const settle of b.pending.values())
|
|
42
|
+
settle(undefined);
|
|
43
|
+
b.pending.clear();
|
|
44
|
+
}
|
|
29
45
|
/** Wraps a live command ctx and fans interactions out to local TUI + browsers. */
|
|
30
46
|
export class SessionUI {
|
|
31
47
|
ctx;
|
|
@@ -42,35 +58,78 @@ export class SessionUI {
|
|
|
42
58
|
}
|
|
43
59
|
/** Race the local input against a remote answer; first to settle wins. */
|
|
44
60
|
async ask(spec) {
|
|
45
|
-
|
|
46
|
-
const id = String(b.nextId++);
|
|
47
|
-
const ac = new AbortController();
|
|
48
|
-
const remote = new Promise(resolve => {
|
|
49
|
-
b.pending.set(id, resolve);
|
|
50
|
-
});
|
|
51
|
-
const prompt = {
|
|
61
|
+
return this.race(id => ({
|
|
52
62
|
type: 'prompt',
|
|
53
63
|
id,
|
|
54
64
|
question: spec.question,
|
|
55
65
|
recommended: spec.recommended,
|
|
56
66
|
...(spec.recommended2 !== undefined && { recommended2: spec.recommended2 }),
|
|
57
|
-
...(spec.actions !== undefined
|
|
67
|
+
...(spec.actions !== undefined
|
|
68
|
+
&& spec.actions.length > 0 && { actions: spec.actions }),
|
|
58
69
|
allowSkip: spec.allowSkip
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
}), spec.question, signal => this.askLocal(spec, signal));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Show text that only needs dismissing. Locally that is the terminal editor;
|
|
74
|
+
* remotely it is a dismiss-only prompt card, and it has to be a prompt rather
|
|
75
|
+
* than a `viewer` because only the prompt slot is in the reconnect snapshot
|
|
76
|
+
* and only a prompt can settle the caller from the browser.
|
|
77
|
+
*
|
|
78
|
+
* pi's `ui.editor` takes no AbortSignal, so a remote dismissal cannot close
|
|
79
|
+
* the terminal editor the way a remote answer aborts a local ask. It is left
|
|
80
|
+
* open and whatever it eventually returns is discarded — the run has moved on.
|
|
81
|
+
*/
|
|
82
|
+
async show(spec) {
|
|
83
|
+
// Nothing to wait for: with no local editor the only surface is the
|
|
84
|
+
// browser, and a DISPLAY that blocks a run until someone opens one is
|
|
85
|
+
// worse than a note nobody dismisses.
|
|
86
|
+
if (!this.ctx.hasUI) {
|
|
87
|
+
publishNote(`${spec.question}\n${spec.body}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await this.race(id => ({
|
|
91
|
+
type: 'prompt',
|
|
92
|
+
id,
|
|
93
|
+
question: spec.question,
|
|
94
|
+
recommended: spec.body,
|
|
95
|
+
dismissOnly: true,
|
|
96
|
+
allowSkip: false
|
|
97
|
+
}),
|
|
98
|
+
// No push: a read-only card is not a request for input, and waking a
|
|
99
|
+
// phone to say it is would be a lie.
|
|
100
|
+
null, () => this.ctx.ui.editor(spec.localTitle, spec.localText));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The shared half of ask() and show(): publish one prompt card, open the
|
|
104
|
+
* local dialog beside it, and take whichever settles first.
|
|
105
|
+
*/
|
|
106
|
+
async race(build, pushBody, local) {
|
|
107
|
+
const b = this.bridge;
|
|
108
|
+
const id = String(b.nextId++);
|
|
109
|
+
const ac = new AbortController();
|
|
110
|
+
const remote = new Promise(resolve => {
|
|
111
|
+
b.pending.set(id, resolve);
|
|
112
|
+
});
|
|
113
|
+
// The slot holds one prompt. Whatever is in it is put BACK when this one
|
|
114
|
+
// settles, or a /task-list would leave a parked question unanswerable
|
|
115
|
+
// from every browser — off the card and out of the snapshot both.
|
|
116
|
+
const displaced = getState().prompt;
|
|
117
|
+
setPrompt(build(id));
|
|
118
|
+
if (pushBody !== null) {
|
|
119
|
+
// Reaches a backgrounded/suspended phone, which the in-page UI can't. The
|
|
120
|
+
// service worker drops the banner if a window is visible+focused (sw.ts),
|
|
121
|
+
// so we always push and let delivery-time visibility decide.
|
|
122
|
+
void pushNotify('pi needs your input', pushBody, 'pi-prompt').catch(() => { });
|
|
123
|
+
}
|
|
65
124
|
// Local: resolves to a value, or to undefined on cancel or abort. The
|
|
66
125
|
// catch is belt-and-braces — a rejection here would surface as an
|
|
67
126
|
// unhandled one rather than as a lost race.
|
|
68
|
-
const
|
|
69
|
-
|
|
127
|
+
const localSide = this.ctx.hasUI ?
|
|
128
|
+
local(ac.signal).catch(() => undefined)
|
|
70
129
|
: new Promise(() => { });
|
|
71
130
|
try {
|
|
72
131
|
const winner = await Promise.race([
|
|
73
|
-
|
|
132
|
+
localSide.then(v => ({ from: 'local', v })),
|
|
74
133
|
remote.then(v => ({ from: 'remote', v }))
|
|
75
134
|
]);
|
|
76
135
|
if (winner.from === 'remote')
|
|
@@ -80,6 +139,8 @@ export class SessionUI {
|
|
|
80
139
|
finally {
|
|
81
140
|
b.pending.delete(id);
|
|
82
141
|
clearPrompt(id);
|
|
142
|
+
if (displaced && b.pending.has(displaced.id))
|
|
143
|
+
setPrompt(displaced);
|
|
83
144
|
}
|
|
84
145
|
}
|
|
85
146
|
/**
|
|
@@ -141,11 +202,61 @@ export function publishLifecycleNotice(message, level) {
|
|
|
141
202
|
else
|
|
142
203
|
publishNotify(message, level);
|
|
143
204
|
}
|
|
144
|
-
|
|
145
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Say it on BOTH surfaces. The local notify is attempted first and its failure
|
|
207
|
+
* absorbed — a stale ctx must not cost the remote the message, which is the one
|
|
208
|
+
* surface still there when nobody is at the terminal.
|
|
209
|
+
*/
|
|
210
|
+
export function notifyBoth(ctx, message, level) {
|
|
211
|
+
try {
|
|
212
|
+
ctx.ui.notify(message, level);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
/* stale ctx — the remote half below is what matters */
|
|
216
|
+
}
|
|
217
|
+
publishLifecycleNotice(message, level);
|
|
218
|
+
}
|
|
219
|
+
/** {@link notifyBoth} for something that must outlive a toast — see
|
|
220
|
+
* {@link publishRunNotice}. */
|
|
221
|
+
export function notifyRun(ctx, message, level) {
|
|
222
|
+
try {
|
|
223
|
+
ctx.ui.notify(message, level);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
/* stale ctx — the remote half below is what matters */
|
|
227
|
+
}
|
|
228
|
+
publishRunNotice(message, level);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* A run outcome the remote must still have later: what a gate decided, what a
|
|
232
|
+
* commit failed to do, what a resume refused. Unlike publishLifecycleNotice it
|
|
233
|
+
* persists at EVERY level, because "still there in the morning" is the reason
|
|
234
|
+
* these are published at all — a warning that vanishes after four seconds tells
|
|
235
|
+
* an absent user nothing.
|
|
236
|
+
*/
|
|
237
|
+
export function publishRunNotice(message, level) {
|
|
238
|
+
if (level === 'error')
|
|
239
|
+
addError(message);
|
|
240
|
+
else
|
|
241
|
+
addSystemNote(message);
|
|
242
|
+
}
|
|
243
|
+
/** Commit text to the remote transcript. Unlike a notify it is in the reconnect
|
|
244
|
+
* snapshot, so it is still there after a refresh. */
|
|
245
|
+
export function publishNote(text) {
|
|
246
|
+
addSystemNote(text);
|
|
146
247
|
}
|
|
147
248
|
// ─── Shimmed ctx ─────────────────────────────────────────────────────────────
|
|
148
249
|
const SHIMMED_MARKER = '__piRemoteShimmed';
|
|
250
|
+
const REMOTE_ORIGIN_MARKER = '__piRemoteOrigin';
|
|
251
|
+
/**
|
|
252
|
+
* True when this ctx reached the handler through the browser rather than the
|
|
253
|
+
* terminal. A command that can only draw itself on the host TUI must check it:
|
|
254
|
+
* awaiting a local overlay for a remote caller parks the browser on a dialog
|
|
255
|
+
* nobody is sitting in front of.
|
|
256
|
+
*/
|
|
257
|
+
export function isRemoteOrigin(ctx) {
|
|
258
|
+
return ctx[REMOTE_ORIGIN_MARKER] === true;
|
|
259
|
+
}
|
|
149
260
|
/**
|
|
150
261
|
* Wraps an event-scoped ExtensionContext so it can be used as a command ctx
|
|
151
262
|
* for commands that don't need newSession (e.g. /task-resume, /task-list,
|
|
@@ -178,7 +289,12 @@ export function registerBridgeCommand(pi, name, def) {
|
|
|
178
289
|
const wrapped = {
|
|
179
290
|
...def,
|
|
180
291
|
handler: (args, ctx) => {
|
|
181
|
-
|
|
292
|
+
// A remote-dispatched ctx is a clone of this very field. Storing it
|
|
293
|
+
// back would make the next remote line clone the clone — one more
|
|
294
|
+
// prototype link per command, for the life of the session — and would
|
|
295
|
+
// leave the marker on the ctx a later TERMINAL command is handed.
|
|
296
|
+
if (!isRemoteOrigin(ctx))
|
|
297
|
+
b.currentCtx = ctx;
|
|
182
298
|
return def.handler(args, ctx);
|
|
183
299
|
}
|
|
184
300
|
};
|
|
@@ -242,8 +358,12 @@ export function dispatchRemoteLine(text, opts) {
|
|
|
242
358
|
// sync throws and async rejections from the (often async) command handler
|
|
243
359
|
// as a toast instead of crashing or becoming an unhandled rejection.
|
|
244
360
|
const toastErr = (err) => publishNotify(`/${name} failed: ${err.message}`, 'error');
|
|
361
|
+
// A prototype clone rather than the ctx itself: the marker must not leak onto
|
|
362
|
+
// the shared currentCtx and make the NEXT terminal invocation look remote.
|
|
363
|
+
const remoteCtx = Object.create(b.currentCtx);
|
|
364
|
+
remoteCtx[REMOTE_ORIGIN_MARKER] = true;
|
|
245
365
|
try {
|
|
246
|
-
const result = handler(args,
|
|
366
|
+
const result = handler(args, remoteCtx);
|
|
247
367
|
if (result instanceof Promise)
|
|
248
368
|
result.catch(toastErr);
|
|
249
369
|
}
|
|
@@ -14,6 +14,14 @@ export interface PromptMessage {
|
|
|
14
14
|
label: string;
|
|
15
15
|
value: string;
|
|
16
16
|
}[];
|
|
17
|
+
/**
|
|
18
|
+
* The card only DISPLAYS. `recommended` carries the body, and the buttons
|
|
19
|
+
* collapse to a single Close that answers with an empty string. Its one
|
|
20
|
+
* producer is /task-plan's answer to a question the USER asked, which needs
|
|
21
|
+
* dismissing rather than answering — and which needs to be a prompt, not a
|
|
22
|
+
* broadcast-only frame, because only a prompt is in the reconnect snapshot.
|
|
23
|
+
*/
|
|
24
|
+
dismissOnly?: boolean;
|
|
17
25
|
allowSkip: boolean;
|
|
18
26
|
}
|
|
19
27
|
export interface PromptResolvedMessage {
|
|
@@ -58,11 +66,6 @@ export interface HeldMessage {
|
|
|
58
66
|
/** True while a task run owns the session (drives the composer placeholder). */
|
|
59
67
|
runActive: boolean;
|
|
60
68
|
}
|
|
61
|
-
export interface ViewerMessage {
|
|
62
|
-
type: 'viewer';
|
|
63
|
-
title: string;
|
|
64
|
-
text: string;
|
|
65
|
-
}
|
|
66
69
|
export interface ContextUsage {
|
|
67
70
|
tokens?: number;
|
|
68
71
|
contextWindow?: number;
|
|
@@ -87,7 +90,7 @@ export type { SnapshotMessage } from './session-state.js';
|
|
|
87
90
|
/** Server → browser messages. The live text_delta / tool_* / agent_* /
|
|
88
91
|
* user_message deltas are emitted by the SessionState mutators
|
|
89
92
|
* and not all enumerated here; the snapshot below carries the full state. */
|
|
90
|
-
export type ServerMessage = PromptMessage | PromptResolvedMessage | WidgetMessage | NotifyMessage |
|
|
93
|
+
export type ServerMessage = PromptMessage | PromptResolvedMessage | WidgetMessage | NotifyMessage | HeldMessage | ContextMessage | ResetMessage | import('./session-state.js').SnapshotMessage;
|
|
91
94
|
/** Browser → server messages. */
|
|
92
95
|
export interface ClientChatMessage {
|
|
93
96
|
type: 'message';
|
package/dist/remote/register.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getConfig } from '../config/config.js';
|
|
2
|
-
import { getBridge, dispatchRemoteLine, dispatchRemoteNewSession, makeShimmedCtx, interruptAgent, registerBridgeCommand, registerRemoteOnlyCommand, publishNotify } from './bridge.js';
|
|
2
|
+
import { getBridge, dispatchRemoteLine, dispatchRemoteNewSession, makeShimmedCtx, interruptAgent, registerBridgeCommand, registerRemoteOnlyCommand, publishNotify, cancelPendingPrompts, notifyBoth } from './bridge.js';
|
|
3
3
|
import { setupEvents } from './events.js';
|
|
4
4
|
import { reset, addUserTurn, setHeld, getState } from './session-state.js';
|
|
5
5
|
import { html } from './ui.js';
|
|
@@ -45,6 +45,12 @@ export function registerRemote(pi) {
|
|
|
45
45
|
return S.server;
|
|
46
46
|
S.server = await startServer(text => {
|
|
47
47
|
if (text === '/new') {
|
|
48
|
+
// The reset inside a new session clears the browser's prompt
|
|
49
|
+
// card, so a parked ask has lost its last surface. Only HERE:
|
|
50
|
+
// session_start also fires for the /task handoff's own
|
|
51
|
+
// newSession, and cancelling there reads to the planner as the
|
|
52
|
+
// user skipping a question they never saw.
|
|
53
|
+
cancelPendingPrompts();
|
|
48
54
|
dispatchRemoteNewSession(newCtx => {
|
|
49
55
|
S.send = (msg, opts) => {
|
|
50
56
|
void (opts ?
|
|
@@ -89,7 +95,7 @@ export function registerRemote(pi) {
|
|
|
89
95
|
if (getConfig().remote) {
|
|
90
96
|
// Optional feature: a bind failure must never take pi down. Degrade
|
|
91
97
|
// to a one-line warning and keep the agent running without remote.
|
|
92
|
-
void ensureServer().catch(err => ctx
|
|
98
|
+
void ensureServer().catch(err => notifyBoth(ctx, `Remote UI unavailable: ${err.message}`, 'warning'));
|
|
93
99
|
}
|
|
94
100
|
});
|
|
95
101
|
pi.on('session_shutdown', (event, _ctx) => {
|
|
@@ -124,7 +130,7 @@ export function registerRemote(pi) {
|
|
|
124
130
|
description: 'Show the remote QR code & URLs.',
|
|
125
131
|
handler: async (args, ctx) => {
|
|
126
132
|
if (!getConfig().remote) {
|
|
127
|
-
ctx
|
|
133
|
+
notifyBoth(ctx, 'Remote is disabled — enable it in /task-config.', 'info');
|
|
128
134
|
return;
|
|
129
135
|
}
|
|
130
136
|
if (args.trim() === 'stop') {
|
|
@@ -134,10 +140,10 @@ export function registerRemote(pi) {
|
|
|
134
140
|
S.server = null;
|
|
135
141
|
S.serveResult = null;
|
|
136
142
|
void teardownTailscaleServe(port).catch(() => { });
|
|
137
|
-
ctx
|
|
143
|
+
notifyBoth(ctx, 'Remote server stopped', 'info');
|
|
138
144
|
}
|
|
139
145
|
else {
|
|
140
|
-
ctx
|
|
146
|
+
notifyBoth(ctx, 'Remote server is not running', 'warning');
|
|
141
147
|
}
|
|
142
148
|
return;
|
|
143
149
|
}
|
|
@@ -193,10 +199,10 @@ export function registerRemote(pi) {
|
|
|
193
199
|
})
|
|
194
200
|
.catch(() => { });
|
|
195
201
|
}
|
|
196
|
-
ctx
|
|
202
|
+
notifyBoth(ctx, `Remote running at ${primaryUrl}`, 'info');
|
|
197
203
|
}
|
|
198
204
|
catch (err) {
|
|
199
|
-
ctx
|
|
205
|
+
notifyBoth(ctx, `Remote UI unavailable: ${err.message}`, 'error');
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
208
|
});
|
|
@@ -198,7 +198,12 @@ export function setPrompt(prompt) {
|
|
|
198
198
|
}
|
|
199
199
|
export function clearPrompt(id) {
|
|
200
200
|
const s = getState();
|
|
201
|
-
|
|
201
|
+
// Only the prompt that is actually on screen. Two asks can be live at once —
|
|
202
|
+
// dispatchRemoteLine starts a command whatever else is running — and the slot
|
|
203
|
+
// holds one, so clearing unconditionally would drop the survivor out of the
|
|
204
|
+
// snapshot and leave a reconnecting browser unable to answer it.
|
|
205
|
+
if (s.prompt?.id === id)
|
|
206
|
+
s.prompt = null;
|
|
202
207
|
s.sink({ type: 'prompt_resolved', id });
|
|
203
208
|
}
|
|
204
209
|
export function setContext(context) {
|
package/dist/remote/ui-script.js
CHANGED
|
@@ -101,9 +101,6 @@ export function clientScript(wsUrl) {
|
|
|
101
101
|
const promptRecText = document.getElementById('prompt-rec-text');
|
|
102
102
|
const promptInput = document.getElementById('prompt-input');
|
|
103
103
|
const promptButtons = document.getElementById('prompt-buttons');
|
|
104
|
-
const viewer = document.getElementById('viewer');
|
|
105
|
-
const viewerBody = document.getElementById('viewer-body');
|
|
106
|
-
document.getElementById('viewer-close').onclick = function () { viewer.style.display = 'none'; };
|
|
107
104
|
let activePromptId = null;
|
|
108
105
|
let activeRecommended = '';
|
|
109
106
|
let activeRecommended2 = '';
|
|
@@ -832,13 +829,16 @@ export function clientScript(wsUrl) {
|
|
|
832
829
|
return out;
|
|
833
830
|
}
|
|
834
831
|
|
|
835
|
-
|
|
832
|
+
// omitCancel is for the dismiss-only card: there is nothing to cancel, and a
|
|
833
|
+
// Cancel task button beside a read-only answer would offer to abort the run
|
|
834
|
+
// when the only real choice is to close.
|
|
835
|
+
function renderButtons(buttons, stacked, omitCancel) {
|
|
836
836
|
promptButtons.className = stacked ? 'row stacked' : 'row';
|
|
837
837
|
promptButtons.innerHTML = '';
|
|
838
838
|
for (let i = 0; i < buttons.length; i++) promptButtons.appendChild(buttons[i]);
|
|
839
839
|
const actions = makeActionBtns();
|
|
840
840
|
for (let i = 0; i < actions.length; i++) promptButtons.appendChild(actions[i]);
|
|
841
|
-
promptButtons.appendChild(makeCancelBtn());
|
|
841
|
+
if (!omitCancel) promptButtons.appendChild(makeCancelBtn());
|
|
842
842
|
}
|
|
843
843
|
|
|
844
844
|
// Manual-entry view: empty textarea + Submit, reachable from the
|
|
@@ -881,7 +881,14 @@ export function clientScript(wsUrl) {
|
|
|
881
881
|
activeRecommended = msg.recommended || '';
|
|
882
882
|
activeRecommended2 = msg.recommended2 || '';
|
|
883
883
|
activeActions = msg.actions || [];
|
|
884
|
-
if (msg.
|
|
884
|
+
if (msg.dismissOnly) {
|
|
885
|
+
// Mode C: nothing to answer. The body goes in the panel so it renders as
|
|
886
|
+
// markdown, and Close is the only way out.
|
|
887
|
+
promptInput.style.display = 'none';
|
|
888
|
+
promptRec.style.display = 'block';
|
|
889
|
+
setContent(promptRecText, msg.recommended || '');
|
|
890
|
+
renderButtons([makeBtn('Close', 'primary', function () { answer(''); })], false, true);
|
|
891
|
+
} else if (msg.recommended) {
|
|
885
892
|
// Mode A: recommendation(s) present. Render markdown in the panel so a
|
|
886
893
|
// recommendation with code/emphasis reads the same as an assistant bubble.
|
|
887
894
|
setContent(promptRecText, msg.recommended);
|
|
@@ -1086,10 +1093,6 @@ export function clientScript(wsUrl) {
|
|
|
1086
1093
|
case 'notify':
|
|
1087
1094
|
showToast(msg.message, msg.level);
|
|
1088
1095
|
break;
|
|
1089
|
-
case 'viewer':
|
|
1090
|
-
viewerBody.textContent = msg.text;
|
|
1091
|
-
viewer.style.display = 'block';
|
|
1092
|
-
break;
|
|
1093
1096
|
case 'reset':
|
|
1094
1097
|
// A new session started — wipe the previous session's transcript.
|
|
1095
1098
|
chatLog.innerHTML = '';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const STYLES = " :root {\n --base: #1e1e2e; --mantle: #181825; --crust: #11111b;\n --surface0: #313244; --surface1: #45475a; --surface2: #585b70;\n --text: #cdd6f4; --subtext1: #a6adc8; --subtext0: #7f849c;\n --mauve: #cba6f7; --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8;\n --yellow: #f9e2af; --peach: #fab387; --teal: #94e2d5;\n }\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n background: var(--base); color: var(--text);\n font-family: ui-monospace, monospace;\n /* --app-h is set by setAppHeight (visualViewport.height, falling back to\n window.innerHeight) so the column height is a stable pixel value across\n an orientation change. 100dvh is the fallback for first paint / no-JS. */\n height: var(--app-h, 100dvh);\n display: flex; flex-direction: column; overflow: hidden;\n padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)\n 0px env(safe-area-inset-left, 0px);\n }\n #context-bar { height: 4px; background: var(--surface0); flex-shrink: 0; }\n #context-bar-fill { height: 100%; background: var(--mauve); width: 0%; transition: width 0.4s ease; }\n #header {\n background: var(--mantle); padding: 8px 16px;\n display: flex; justify-content: space-between; align-items: center;\n font-size: 13px; flex-shrink: 0; border-bottom: 1px solid var(--surface0);\n }\n #header .title { font-weight: bold; color: var(--mauve); letter-spacing: 0.05em;\n position: relative; animation: glitch 5s steps(1) infinite; }\n @keyframes glitch {\n 0%, 88%, 100% { text-shadow: none; transform: translate(0, 0); }\n 90% { text-shadow: -1px 0 var(--red), 1px 0 var(--teal); transform: translate(1px, -1px); }\n 92% { text-shadow: 1px 0 var(--red), -1px 0 var(--blue); transform: translate(-1px, 1px); }\n 94% { text-shadow: -1px 0 var(--blue), 1px 0 var(--red); transform: translate(1px, 0); }\n 96% { text-shadow: 1px 0 var(--teal), -1px 0 var(--red); transform: translate(-1px, 0); }\n }\n @media (prefers-reduced-motion: reduce) { #header .title { animation: none; } }\n #header .hgroup { display: flex; align-items: center; gap: 10px; }\n #bell {\n background: none; border: none; color: var(--subtext1); cursor: pointer;\n font-size: 15px; line-height: 1; padding: 2px; font-family: inherit;\n }\n #bell:hover { color: var(--text); }\n #bell.on { color: var(--mauve); }\n /* Header status chip: connection dot + model name + context usage. */\n #status-chip { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--subtext0); }\n #status-dot {\n width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;\n background: var(--surface2); transition: background 0.2s ease;\n }\n #status-dot.idle { background: var(--green); }\n #status-dot.running { background: var(--mauve); animation: dot-pulse 1.2s ease-in-out infinite; }\n #status-dot.disconnected { background: var(--red); }\n @keyframes dot-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }\n @media (prefers-reduced-motion: reduce) { #status-dot.running { animation: none; } }\n #status-model { color: var(--subtext1); }\n #status-model:empty { display: none; }\n #status-ctx { color: var(--subtext0); font-variant-numeric: tabular-nums; }\n #status-ctx:empty { display: none; }\n /* Notification bell dropdown: a push toggle row + the recent-toast history. */\n #notif-panel {\n display: none; position: fixed; z-index: 80;\n top: calc(env(safe-area-inset-top, 0px) + 42px);\n right: calc(env(safe-area-inset-right, 0px) + 12px);\n width: min(320px, calc(100vw - 24px));\n background: var(--mantle); border: 1px solid var(--surface1); border-radius: 8px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); overflow: hidden;\n }\n #notif-panel.open { display: block; }\n #notif-toggle-row {\n display: flex; align-items: center; justify-content: space-between;\n padding: 8px 12px; border-bottom: 1px solid var(--surface0);\n }\n #notif-title { font-size: 12px; color: var(--subtext1); font-weight: 700; }\n #notif-toggle {\n background: var(--surface1); color: var(--text); border: none; border-radius: 6px;\n padding: 4px 10px; font-family: inherit; font-size: 11px; cursor: pointer;\n }\n #notif-toggle:hover { filter: brightness(1.1); }\n #notif-toggle.on { background: var(--mauve); color: var(--crust); font-weight: 700; }\n #notif-list { max-height: 40dvh; overflow-y: auto; }\n #notif-empty { padding: 14px 12px; color: var(--subtext0); font-size: 11px; text-align: center; }\n .notif-item {\n display: flex; align-items: baseline; gap: 8px; padding: 7px 12px;\n border-bottom: 1px solid var(--surface0); font-size: 12px;\n }\n .notif-item:last-child { border-bottom: none; }\n .notif-item .notif-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--blue); align-self: center; }\n .notif-item.warning .notif-dot { background: var(--peach); }\n .notif-item.error .notif-dot { background: var(--red); }\n .notif-item .notif-msg { flex: 1; min-width: 0; color: var(--text); overflow-wrap: anywhere; word-break: break-word; }\n .notif-item .notif-time { flex-shrink: 0; color: var(--subtext0); font-size: 10px; font-variant-numeric: tabular-nums; }\n #chat-wrap { position: relative; flex: 1; min-height: 0; display: flex; }\n #chat-log {\n flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 16px;\n display: flex; flex-direction: column; gap: 8px;\n }\n /* Floating jump-to-latest button \u2014 only shown when scrolled away from the\n bottom (toggled via .visible from the scroll handler). */\n #scroll-bottom {\n display: none; position: absolute; bottom: 16px; right: 16px; z-index: 40;\n width: 36px; height: 36px; border-radius: 50%; cursor: pointer;\n background: var(--surface1); color: var(--text); border: 1px solid var(--surface2);\n font-family: inherit; font-size: 18px; line-height: 1; padding: 0;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n #scroll-bottom:hover { background: var(--surface2); color: var(--mauve); }\n #scroll-bottom.visible { display: block; }\n #chat-log::-webkit-scrollbar { width: 6px; }\n #chat-log::-webkit-scrollbar-track { background: transparent; }\n #chat-log::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 3px; }\n .bubble {\n max-width: 82%; padding: 8px 12px; border-radius: 8px;\n line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 13px;\n }\n .bubble.user { background: var(--surface1); color: var(--text); align-self: flex-end; }\n .bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; position: relative; }\n .bubble.error {\n background: var(--crust); color: var(--red); align-self: stretch;\n max-width: 100%; border: 1px solid var(--red); font-size: 12px;\n }\n /* Persistent inline system note (e.g. context compaction) \u2014 a muted centered\n divider, distinct from chat bubbles. */\n .sysnote {\n align-self: center; color: var(--subtext0); font-size: 11px;\n font-family: ui-monospace, monospace; letter-spacing: 0.5px;\n padding: 2px 10px; opacity: 0.85;\n }\n .bubble.thinking {\n display: flex; gap: 5px; align-items: center; padding: 10px 14px;\n }\n .bubble.thinking .spinner {\n color: var(--mauve); font-size: 15px; line-height: 1;\n font-family: ui-monospace, monospace;\n }\n /* Collapsed reasoning block (\"\u273B Thinking\u2026 (n lines)\"), muted + italic. */\n .thinking-block { align-self: flex-start; max-width: 90%; font-size: 12px; }\n .thinking-block > summary {\n color: var(--subtext0); font-style: italic; cursor: pointer; list-style: none;\n user-select: none; display: flex; align-items: center; gap: 8px; padding: 2px 0;\n }\n .thinking-block > summary::-webkit-details-marker { display: none; }\n .thinking-block .thinking-spin {\n color: var(--mauve); font-style: normal; font-family: ui-monospace, monospace;\n }\n .thinking-block .thinking-body {\n color: var(--subtext0); font-style: italic; white-space: pre-wrap;\n word-break: break-word; line-height: 1.5; margin: 4px 0 0 4px;\n padding: 4px 0 2px 12px; border-left: 2px solid var(--surface1);\n }\n /* Copy buttons: on code-block headers and (floating) on finished assistant\n bubbles. Wired by one delegated click handler in the client script. */\n .copy-btn {\n background: transparent; border: none; color: var(--subtext0); cursor: pointer;\n font-family: inherit; font-size: 11px; padding: 2px 6px; border-radius: 4px;\n line-height: 1.4;\n }\n .copy-btn:hover { color: var(--text); background: var(--surface1); }\n .copy-btn.copied { color: var(--green); }\n .bubble-copy {\n position: absolute; top: 4px; right: 4px; opacity: 0;\n background: var(--surface1); transition: opacity 0.12s ease;\n }\n .bubble.assistant:hover .bubble-copy { opacity: 1; }\n /* Touch devices have no hover \u2014 keep the button faintly visible. */\n @media (hover: none) { .bubble-copy { opacity: 0.55; } }\n .tool-call {\n background: var(--crust); border-radius: 6px; align-self: flex-start;\n max-width: 90%; font-size: 12px; border: 1px solid var(--surface0);\n }\n .tool-call summary {\n padding: 6px 10px; color: var(--subtext1); cursor: pointer;\n user-select: none; list-style: none;\n display: flex; align-items: center; gap: 8px;\n }\n .tool-call summary::-webkit-details-marker { display: none; }\n .tool-call summary::before { content: \"\u25B6\"; flex-shrink: 0; font-size: 9px; color: var(--subtext0); }\n .tool-call[open] > summary::before { content: \"\u25BC\"; }\n .tool-call.error > summary { color: var(--red); }\n /* The summary is a single line: the label ellipsizes (full text in the title\n tooltip / on expand), badges and timing stay pinned to the right. */\n .tool-label {\n flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-family: ui-monospace, monospace;\n }\n .tool-badge { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-elapsed { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-call pre {\n padding: 8px 12px; overflow-y: auto;\n color: var(--subtext1); font-size: 11px; max-height: 280px;\n border-top: 1px solid var(--surface0);\n white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-spin { color: var(--mauve); flex-shrink: 0; font-family: ui-monospace, monospace; font-size: 13px; }\n /* Line diff for edit/write tools (tints derived from --green/--red). */\n .tool-diff { border-top: 1px solid var(--surface0); overflow-x: auto; }\n .diff { font-family: ui-monospace, monospace; font-size: 11px; padding: 4px 0; }\n .diff-line { white-space: pre; padding: 0 10px; }\n .diff-sign { display: inline-block; width: 1ch; margin-right: 8px; color: var(--subtext0); }\n .diff-add { background: color-mix(in srgb, var(--green) 14%, transparent); color: var(--green); }\n .diff-del { background: color-mix(in srgb, var(--red) 14%, transparent); color: var(--red); }\n .diff-ctx { color: var(--subtext1); }\n .code-block {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 6px; overflow: hidden; margin: 4px 0;\n align-self: stretch; max-width: 100%; font-size: 12px;\n }\n /* Header row above a code block: language label on the left, copy button on\n the right, on its own surface bar. */\n .code-head {\n display: flex; align-items: center; justify-content: space-between;\n background: var(--surface0);\n }\n .code-lang {\n color: var(--subtext0); font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;\n }\n .code-head .copy-btn { padding: 3px 10px; }\n .code-block code {\n display: block; padding: 10px 12px; overflow-x: auto;\n color: var(--text); white-space: pre; line-height: 1.55;\n }\n .hl-kw { color: var(--mauve); }\n .hl-str { color: var(--green); }\n .hl-cmt { color: var(--subtext0); font-style: italic; }\n .hl-num { color: var(--blue); }\n .hl-fn { color: var(--yellow); }\n /* Hand-rolled markdown, applied only to assistant bubbles + the recommendation\n panel (both get the .md class from setContent). Block elements lay themselves\n out, so switch off the container's pre-wrap for these. */\n .md { white-space: normal; }\n .md > :first-child { margin-top: 0; }\n .md > :last-child { margin-bottom: 0; }\n .md .md-h { color: var(--mauve); font-weight: 700; line-height: 1.3; margin: 10px 0 4px; }\n .md .md-h1 { font-size: 1.35em; }\n .md .md-h2 { font-size: 1.2em; }\n .md .md-h3 { font-size: 1.08em; }\n .md .md-h4, .md .md-h5, .md .md-h6 { font-size: 1em; }\n .md .md-p { margin: 6px 0; }\n .md .md-list { margin: 6px 0; padding-left: 22px; }\n .md .md-list li { margin: 2px 0; }\n .md .md-task { list-style: none; margin-left: -22px; }\n .md .md-check { color: var(--green); }\n .md .md-quote { border-left: 3px solid var(--surface2); margin: 6px 0; padding: 2px 0 2px 10px; color: var(--subtext1); }\n .md .md-hr { border: none; border-top: 1px solid var(--surface1); margin: 10px 0; }\n .md a { color: var(--blue); text-decoration: underline; }\n .md strong { color: var(--text); font-weight: 700; }\n .md em { font-style: italic; }\n .md del { color: var(--subtext0); }\n .md .md-code {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 4px; padding: 1px 4px; font-size: 0.92em;\n }\n .md .md-table { border-collapse: collapse; margin: 8px 0; font-size: 0.95em; display: block; overflow-x: auto; }\n .md .md-table th, .md .md-table td { border: 1px solid var(--surface1); padding: 4px 8px; text-align: left; }\n .md .md-table th { background: var(--surface0); color: var(--subtext1); }\n #input-bar {\n background: var(--mantle); padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));\n display: flex; gap: 8px; flex-shrink: 0;\n border-top: 1px solid var(--surface0);\n position: relative;\n }\n /* Held mid-run input: what you typed is waiting for the next task turn.\n In normal flow, NOT absolutely positioned \u2014 as an overlay it would cover\n the task widget's progress row above the composer. */\n #held-bar {\n display: flex; align-items: center; gap: 8px; flex-shrink: 0;\n background: var(--mantle); border-top: 1px solid var(--surface1);\n padding: 6px 16px; font-size: 12px;\n }\n #held-label { color: var(--yellow); white-space: nowrap; }\n #held-text {\n color: var(--subtext0); overflow: hidden; text-overflow: ellipsis;\n white-space: nowrap; flex: 1;\n }\n #held-clear {\n background: none; border: none; color: var(--subtext0);\n cursor: pointer; font-size: 13px; padding: 0 2px;\n }\n #held-clear:hover { color: var(--red); }\n #cmd-suggestions {\n display: none; position: absolute; bottom: 100%; left: 16px; right: 16px;\n background: var(--mantle); border: 1px solid var(--surface1);\n border-bottom: none; border-radius: 8px 8px 0 0;\n overflow: hidden; z-index: 10;\n }\n .cmd-item {\n display: flex; align-items: baseline; gap: 10px;\n padding: 7px 12px; cursor: pointer; font-size: 12px;\n border-bottom: 1px solid var(--surface0);\n }\n .cmd-item:last-child { border-bottom: none; }\n .cmd-item:hover, .cmd-item.active { background: var(--surface0); }\n .cmd-item .cmd-name { color: var(--blue); font-weight: bold; flex-shrink: 0; }\n .cmd-item .cmd-desc { color: var(--subtext0); }\n #input {\n flex: 1; background: var(--surface0); color: var(--text);\n border: none; border-radius: 6px; padding: 8px 12px;\n font-family: inherit; font-size: 13px; resize: none;\n outline: none; line-height: 1.5; min-height: 36px; max-height: 120px;\n }\n #input::placeholder { color: var(--subtext0); }\n #input:focus { box-shadow: 0 0 0 1px var(--mauve); }\n #send {\n background: var(--blue); color: var(--crust); border: none;\n border-radius: 6px; padding: 8px 16px; font-weight: bold;\n cursor: pointer; font-size: 13px; font-family: inherit;\n white-space: nowrap; align-self: flex-end;\n }\n #send:disabled, #input:disabled { opacity: 0.45; cursor: not-allowed; }\n /* While the agent runs, Send becomes a red Stop; the armed (tap-to-confirm)\n state brightens it and adds a halo, mirroring the prompt card's cancel. */\n #send.stop { background: var(--red); color: var(--crust); }\n #send.stop.armed {\n filter: brightness(1.12);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 45%, transparent);\n }\n #reconnect-overlay {\n display: none; position: fixed; inset: 0;\n background: rgba(30,30,46,0.88); color: var(--subtext1);\n justify-content: center; align-items: center;\n font-size: 13px; z-index: 100; letter-spacing: 0.03em;\n }\n #reconnect-overlay.visible { display: flex; }\n /* Trailing stream indicator: the same braille spinner as the thinking bubble\n (one ticker paints every .spin), inline at the end of the streaming text. */\n .cursor {\n color: var(--mauve); margin-left: 2px;\n font-family: ui-monospace, monospace;\n }\n #status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);\n color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }\n /* Structured task widget (progress bar + phase badge + elapsed). Replaces the\n plain text lines when the server sends a structured data payload. */\n #status-panel.structured { white-space: normal; display: block; }\n /* Title gets its own line and wraps, clamped to 2 \u2014 never squeezed to a stub\n by the meta row beside it on a narrow screen. */\n .widget-title { display: -webkit-box; -webkit-box-orient: vertical;\n -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; color: var(--text);\n font-size: 13px; line-height: 1.3; word-break: break-word; }\n .widget-meta { display: flex; align-items: center; gap: 8px; margin-top: 5px; }\n .widget-phase { flex-shrink: 0; background: var(--surface0); color: var(--mauve);\n border-radius: 4px; padding: 1px 7px; font-size: 10px; letter-spacing: 0.03em;\n text-transform: uppercase; }\n .widget-step { flex-shrink: 0; color: var(--subtext0); font-size: 11px;\n font-variant-numeric: tabular-nums; }\n .widget-elapsed { flex-shrink: 0; margin-left: auto; color: var(--subtext0);\n font-size: 11px; font-variant-numeric: tabular-nums; }\n .widget-bar { height: 4px; background: var(--surface0); border-radius: 2px;\n overflow: hidden; margin-top: 6px; }\n /* Fill carries a soft highlight band that sweeps left\u2192right through the mauve,\n so the bar reads as \"working\" even while the step count holds still. The\n gradient tile is 2x the fill width and shifts one full period per cycle,\n so the loop is seamless. Base color stays mauve for reduced-motion. */\n .widget-bar-fill { height: 100%; border-radius: 2px;\n background: linear-gradient(90deg,\n var(--mauve) 0%, var(--mauve) 38%, #ecdcfd 50%,\n var(--mauve) 62%, var(--mauve) 100%);\n background-size: 200% 100%;\n box-shadow: 0 0 6px rgba(203, 166, 247, 0.45);\n animation: bar-flow 2.4s linear infinite;\n transition: width 0.3s ease; }\n @keyframes bar-flow {\n from { background-position: 200% 0; }\n to { background-position: 0% 0; }\n }\n @media (prefers-reduced-motion: reduce) {\n .widget-bar-fill { animation: none; background: var(--mauve); }\n }\n /* Current action \u2014 the terminal's \u21B3 worker trailer, one dim ellipsized line. */\n .widget-action { margin-top: 6px; color: var(--subtext0); font-size: 11px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-variant-numeric: tabular-nums; }\n /* Dim per-turn timestamp shown under a committed turn's bubble. */\n .turn-time { font-size: 10px; color: var(--subtext0); opacity: 0.55; padding: 0 4px;\n margin-top: -2px; }\n .turn-time.user { align-self: flex-end; }\n .turn-time.assistant, .turn-time.system { align-self: flex-start; }\n .turn-time.system { align-self: center; }\n #prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);\n border-top: 2px solid var(--mauve); padding: 16px 14px calc(16px + env(safe-area-inset-bottom, 0px));\n display: none; z-index: 50; max-height: 80dvh; overflow-y: auto; }\n #prompt-card .q-label { color: var(--mauve); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px; }\n #prompt-card .q { color: var(--text); margin-bottom: 12px; white-space: pre-wrap;\n font-size: 15px; line-height: 1.5; }\n #prompt-card .rec-panel { background: var(--surface0); border-left: 3px solid var(--green);\n border-radius: 6px; padding: 10px 12px; margin-bottom: 12px; }\n #prompt-card .rec-label { color: var(--green); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; }\n #prompt-card .rec-text { color: var(--text); font-size: 15px; line-height: 1.5;\n white-space: pre-wrap; overflow-wrap: anywhere; }\n #prompt-card textarea { width: 100%; background: var(--surface0); color: var(--text);\n border: 1px solid var(--surface2); border-radius: 6px; padding: 10px; font-size: 15px;\n font-family: inherit; line-height: 1.5; resize: vertical; margin-bottom: 4px; }\n #prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;\n flex-wrap: wrap; }\n /* Recommendation answers can be long sentences, so stack them as a readable list. */\n #prompt-card .row.stacked { flex-direction: column; align-items: stretch; }\n #prompt-card .row.stacked button { flex: none; text-align: left; }\n #prompt-card .row.stacked button.cancel { align-self: center; text-align: center; }\n #prompt-card button { padding: 11px 16px; border-radius: 8px; border: none; cursor: pointer;\n font-family: inherit; font-size: 14px; font-weight: 600; transition: filter .15s ease; }\n #prompt-card button:hover { filter: brightness(1.08); }\n #prompt-card button.primary { background: var(--green); color: var(--crust);\n font-weight: 700; flex: 1; min-width: 160px; }\n #prompt-card button.secondary { background: var(--surface1); color: var(--text);\n flex: 1; min-width: 160px; }\n #prompt-card button.cancel { margin-left: auto; align-self: center; background: transparent;\n color: var(--subtext0); font-size: 12px; font-weight: 500; padding: 8px 10px; }\n #prompt-card button.cancel:hover { color: var(--red); filter: none; }\n #prompt-card button.cancel.armed { background: var(--red); color: var(--crust); font-weight: 700; }\n .toast { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 12px);\n right: calc(env(safe-area-inset-right, 0px) + 12px); max-width: calc(100vw - 24px);\n padding: 8px 12px; border-radius: 6px; overflow-wrap: anywhere; word-break: break-word;\n background: var(--surface1); color: var(--text); z-index: 60; }\n .toast.warning { background: var(--peach); color: var(--crust); }\n .toast.error { background: var(--red); color: var(--crust); }\n #viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);\n border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap;\n overflow-wrap: anywhere; word-break: break-word; display: none; z-index: 70; }\n #viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }\n /* Desktop: center the transcript/status/input in a readable column instead of\n hugging the left edge. The scrollbar stays at the true window edge; only the\n content is inset. Mobile (below 960px) is unchanged. */\n @media (min-width: 960px) {\n #chat-log { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #status-panel { padding-left: calc((100% - 920px) / 2 + 12px); padding-right: calc((100% - 920px) / 2 + 12px); }\n #input-bar { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #cmd-suggestions { left: calc((100% - 920px) / 2 + 16px); right: calc((100% - 920px) / 2 + 16px); }\n #held-bar { padding-left: calc((100% - 920px) / 2 + 16px); padding-right: calc((100% - 920px) / 2 + 16px); }\n }";
|
|
1
|
+
export declare const STYLES = " :root {\n --base: #1e1e2e; --mantle: #181825; --crust: #11111b;\n --surface0: #313244; --surface1: #45475a; --surface2: #585b70;\n --text: #cdd6f4; --subtext1: #a6adc8; --subtext0: #7f849c;\n --mauve: #cba6f7; --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8;\n --yellow: #f9e2af; --peach: #fab387; --teal: #94e2d5;\n }\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n background: var(--base); color: var(--text);\n font-family: ui-monospace, monospace;\n /* --app-h is set by setAppHeight (visualViewport.height, falling back to\n window.innerHeight) so the column height is a stable pixel value across\n an orientation change. 100dvh is the fallback for first paint / no-JS. */\n height: var(--app-h, 100dvh);\n display: flex; flex-direction: column; overflow: hidden;\n padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)\n 0px env(safe-area-inset-left, 0px);\n }\n #context-bar { height: 4px; background: var(--surface0); flex-shrink: 0; }\n #context-bar-fill { height: 100%; background: var(--mauve); width: 0%; transition: width 0.4s ease; }\n #header {\n background: var(--mantle); padding: 8px 16px;\n display: flex; justify-content: space-between; align-items: center;\n font-size: 13px; flex-shrink: 0; border-bottom: 1px solid var(--surface0);\n }\n #header .title { font-weight: bold; color: var(--mauve); letter-spacing: 0.05em;\n position: relative; animation: glitch 5s steps(1) infinite; }\n @keyframes glitch {\n 0%, 88%, 100% { text-shadow: none; transform: translate(0, 0); }\n 90% { text-shadow: -1px 0 var(--red), 1px 0 var(--teal); transform: translate(1px, -1px); }\n 92% { text-shadow: 1px 0 var(--red), -1px 0 var(--blue); transform: translate(-1px, 1px); }\n 94% { text-shadow: -1px 0 var(--blue), 1px 0 var(--red); transform: translate(1px, 0); }\n 96% { text-shadow: 1px 0 var(--teal), -1px 0 var(--red); transform: translate(-1px, 0); }\n }\n @media (prefers-reduced-motion: reduce) { #header .title { animation: none; } }\n #header .hgroup { display: flex; align-items: center; gap: 10px; }\n #bell {\n background: none; border: none; color: var(--subtext1); cursor: pointer;\n font-size: 15px; line-height: 1; padding: 2px; font-family: inherit;\n }\n #bell:hover { color: var(--text); }\n #bell.on { color: var(--mauve); }\n /* Header status chip: connection dot + model name + context usage. */\n #status-chip { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--subtext0); }\n #status-dot {\n width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;\n background: var(--surface2); transition: background 0.2s ease;\n }\n #status-dot.idle { background: var(--green); }\n #status-dot.running { background: var(--mauve); animation: dot-pulse 1.2s ease-in-out infinite; }\n #status-dot.disconnected { background: var(--red); }\n @keyframes dot-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }\n @media (prefers-reduced-motion: reduce) { #status-dot.running { animation: none; } }\n #status-model { color: var(--subtext1); }\n #status-model:empty { display: none; }\n #status-ctx { color: var(--subtext0); font-variant-numeric: tabular-nums; }\n #status-ctx:empty { display: none; }\n /* Notification bell dropdown: a push toggle row + the recent-toast history. */\n #notif-panel {\n display: none; position: fixed; z-index: 80;\n top: calc(env(safe-area-inset-top, 0px) + 42px);\n right: calc(env(safe-area-inset-right, 0px) + 12px);\n width: min(320px, calc(100vw - 24px));\n background: var(--mantle); border: 1px solid var(--surface1); border-radius: 8px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); overflow: hidden;\n }\n #notif-panel.open { display: block; }\n #notif-toggle-row {\n display: flex; align-items: center; justify-content: space-between;\n padding: 8px 12px; border-bottom: 1px solid var(--surface0);\n }\n #notif-title { font-size: 12px; color: var(--subtext1); font-weight: 700; }\n #notif-toggle {\n background: var(--surface1); color: var(--text); border: none; border-radius: 6px;\n padding: 4px 10px; font-family: inherit; font-size: 11px; cursor: pointer;\n }\n #notif-toggle:hover { filter: brightness(1.1); }\n #notif-toggle.on { background: var(--mauve); color: var(--crust); font-weight: 700; }\n #notif-list { max-height: 40dvh; overflow-y: auto; }\n #notif-empty { padding: 14px 12px; color: var(--subtext0); font-size: 11px; text-align: center; }\n .notif-item {\n display: flex; align-items: baseline; gap: 8px; padding: 7px 12px;\n border-bottom: 1px solid var(--surface0); font-size: 12px;\n }\n .notif-item:last-child { border-bottom: none; }\n .notif-item .notif-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--blue); align-self: center; }\n .notif-item.warning .notif-dot { background: var(--peach); }\n .notif-item.error .notif-dot { background: var(--red); }\n .notif-item .notif-msg { flex: 1; min-width: 0; color: var(--text); overflow-wrap: anywhere; word-break: break-word; }\n .notif-item .notif-time { flex-shrink: 0; color: var(--subtext0); font-size: 10px; font-variant-numeric: tabular-nums; }\n #chat-wrap { position: relative; flex: 1; min-height: 0; display: flex; }\n #chat-log {\n flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 16px;\n display: flex; flex-direction: column; gap: 8px;\n }\n /* Floating jump-to-latest button \u2014 only shown when scrolled away from the\n bottom (toggled via .visible from the scroll handler). */\n #scroll-bottom {\n display: none; position: absolute; bottom: 16px; right: 16px; z-index: 40;\n width: 36px; height: 36px; border-radius: 50%; cursor: pointer;\n background: var(--surface1); color: var(--text); border: 1px solid var(--surface2);\n font-family: inherit; font-size: 18px; line-height: 1; padding: 0;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n #scroll-bottom:hover { background: var(--surface2); color: var(--mauve); }\n #scroll-bottom.visible { display: block; }\n #chat-log::-webkit-scrollbar { width: 6px; }\n #chat-log::-webkit-scrollbar-track { background: transparent; }\n #chat-log::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 3px; }\n .bubble {\n max-width: 82%; padding: 8px 12px; border-radius: 8px;\n line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 13px;\n }\n .bubble.user { background: var(--surface1); color: var(--text); align-self: flex-end; }\n .bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; position: relative; }\n .bubble.error {\n background: var(--crust); color: var(--red); align-self: stretch;\n max-width: 100%; border: 1px solid var(--red); font-size: 12px;\n }\n /* Persistent inline system note (e.g. context compaction) \u2014 a muted centered\n divider, distinct from chat bubbles. */\n .sysnote {\n /* publishNote sends multi-line text (the /task-config table); textContent\n keeps the newlines only if the box does not collapse them. */\n white-space: pre-wrap;\n align-self: center; color: var(--subtext0); font-size: 11px;\n font-family: ui-monospace, monospace; letter-spacing: 0.5px;\n padding: 2px 10px; opacity: 0.85;\n }\n .bubble.thinking {\n display: flex; gap: 5px; align-items: center; padding: 10px 14px;\n }\n .bubble.thinking .spinner {\n color: var(--mauve); font-size: 15px; line-height: 1;\n font-family: ui-monospace, monospace;\n }\n /* Collapsed reasoning block (\"\u273B Thinking\u2026 (n lines)\"), muted + italic. */\n .thinking-block { align-self: flex-start; max-width: 90%; font-size: 12px; }\n .thinking-block > summary {\n color: var(--subtext0); font-style: italic; cursor: pointer; list-style: none;\n user-select: none; display: flex; align-items: center; gap: 8px; padding: 2px 0;\n }\n .thinking-block > summary::-webkit-details-marker { display: none; }\n .thinking-block .thinking-spin {\n color: var(--mauve); font-style: normal; font-family: ui-monospace, monospace;\n }\n .thinking-block .thinking-body {\n color: var(--subtext0); font-style: italic; white-space: pre-wrap;\n word-break: break-word; line-height: 1.5; margin: 4px 0 0 4px;\n padding: 4px 0 2px 12px; border-left: 2px solid var(--surface1);\n }\n /* Copy buttons: on code-block headers and (floating) on finished assistant\n bubbles. Wired by one delegated click handler in the client script. */\n .copy-btn {\n background: transparent; border: none; color: var(--subtext0); cursor: pointer;\n font-family: inherit; font-size: 11px; padding: 2px 6px; border-radius: 4px;\n line-height: 1.4;\n }\n .copy-btn:hover { color: var(--text); background: var(--surface1); }\n .copy-btn.copied { color: var(--green); }\n .bubble-copy {\n position: absolute; top: 4px; right: 4px; opacity: 0;\n background: var(--surface1); transition: opacity 0.12s ease;\n }\n .bubble.assistant:hover .bubble-copy { opacity: 1; }\n /* Touch devices have no hover \u2014 keep the button faintly visible. */\n @media (hover: none) { .bubble-copy { opacity: 0.55; } }\n .tool-call {\n background: var(--crust); border-radius: 6px; align-self: flex-start;\n max-width: 90%; font-size: 12px; border: 1px solid var(--surface0);\n }\n .tool-call summary {\n padding: 6px 10px; color: var(--subtext1); cursor: pointer;\n user-select: none; list-style: none;\n display: flex; align-items: center; gap: 8px;\n }\n .tool-call summary::-webkit-details-marker { display: none; }\n .tool-call summary::before { content: \"\u25B6\"; flex-shrink: 0; font-size: 9px; color: var(--subtext0); }\n .tool-call[open] > summary::before { content: \"\u25BC\"; }\n .tool-call.error > summary { color: var(--red); }\n /* The summary is a single line: the label ellipsizes (full text in the title\n tooltip / on expand), badges and timing stay pinned to the right. */\n .tool-label {\n flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-family: ui-monospace, monospace;\n }\n .tool-badge { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-elapsed { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-call pre {\n padding: 8px 12px; overflow-y: auto;\n color: var(--subtext1); font-size: 11px; max-height: 280px;\n border-top: 1px solid var(--surface0);\n white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-spin { color: var(--mauve); flex-shrink: 0; font-family: ui-monospace, monospace; font-size: 13px; }\n /* Line diff for edit/write tools (tints derived from --green/--red). */\n .tool-diff { border-top: 1px solid var(--surface0); overflow-x: auto; }\n .diff { font-family: ui-monospace, monospace; font-size: 11px; padding: 4px 0; }\n .diff-line { white-space: pre; padding: 0 10px; }\n .diff-sign { display: inline-block; width: 1ch; margin-right: 8px; color: var(--subtext0); }\n .diff-add { background: color-mix(in srgb, var(--green) 14%, transparent); color: var(--green); }\n .diff-del { background: color-mix(in srgb, var(--red) 14%, transparent); color: var(--red); }\n .diff-ctx { color: var(--subtext1); }\n .code-block {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 6px; overflow: hidden; margin: 4px 0;\n align-self: stretch; max-width: 100%; font-size: 12px;\n }\n /* Header row above a code block: language label on the left, copy button on\n the right, on its own surface bar. */\n .code-head {\n display: flex; align-items: center; justify-content: space-between;\n background: var(--surface0);\n }\n .code-lang {\n color: var(--subtext0); font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;\n }\n .code-head .copy-btn { padding: 3px 10px; }\n .code-block code {\n display: block; padding: 10px 12px; overflow-x: auto;\n color: var(--text); white-space: pre; line-height: 1.55;\n }\n .hl-kw { color: var(--mauve); }\n .hl-str { color: var(--green); }\n .hl-cmt { color: var(--subtext0); font-style: italic; }\n .hl-num { color: var(--blue); }\n .hl-fn { color: var(--yellow); }\n /* Hand-rolled markdown, applied only to assistant bubbles + the recommendation\n panel (both get the .md class from setContent). Block elements lay themselves\n out, so switch off the container's pre-wrap for these. */\n .md { white-space: normal; }\n .md > :first-child { margin-top: 0; }\n .md > :last-child { margin-bottom: 0; }\n .md .md-h { color: var(--mauve); font-weight: 700; line-height: 1.3; margin: 10px 0 4px; }\n .md .md-h1 { font-size: 1.35em; }\n .md .md-h2 { font-size: 1.2em; }\n .md .md-h3 { font-size: 1.08em; }\n .md .md-h4, .md .md-h5, .md .md-h6 { font-size: 1em; }\n .md .md-p { margin: 6px 0; }\n .md .md-list { margin: 6px 0; padding-left: 22px; }\n .md .md-list li { margin: 2px 0; }\n .md .md-task { list-style: none; margin-left: -22px; }\n .md .md-check { color: var(--green); }\n .md .md-quote { border-left: 3px solid var(--surface2); margin: 6px 0; padding: 2px 0 2px 10px; color: var(--subtext1); }\n .md .md-hr { border: none; border-top: 1px solid var(--surface1); margin: 10px 0; }\n .md a { color: var(--blue); text-decoration: underline; }\n .md strong { color: var(--text); font-weight: 700; }\n .md em { font-style: italic; }\n .md del { color: var(--subtext0); }\n .md .md-code {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 4px; padding: 1px 4px; font-size: 0.92em;\n }\n .md .md-table { border-collapse: collapse; margin: 8px 0; font-size: 0.95em; display: block; overflow-x: auto; }\n .md .md-table th, .md .md-table td { border: 1px solid var(--surface1); padding: 4px 8px; text-align: left; }\n .md .md-table th { background: var(--surface0); color: var(--subtext1); }\n #input-bar {\n background: var(--mantle); padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));\n display: flex; gap: 8px; flex-shrink: 0;\n border-top: 1px solid var(--surface0);\n position: relative;\n }\n /* Held mid-run input: what you typed is waiting for the next task turn.\n In normal flow, NOT absolutely positioned \u2014 as an overlay it would cover\n the task widget's progress row above the composer. */\n #held-bar {\n display: flex; align-items: center; gap: 8px; flex-shrink: 0;\n background: var(--mantle); border-top: 1px solid var(--surface1);\n padding: 6px 16px; font-size: 12px;\n }\n #held-label { color: var(--yellow); white-space: nowrap; }\n #held-text {\n color: var(--subtext0); overflow: hidden; text-overflow: ellipsis;\n white-space: nowrap; flex: 1;\n }\n #held-clear {\n background: none; border: none; color: var(--subtext0);\n cursor: pointer; font-size: 13px; padding: 0 2px;\n }\n #held-clear:hover { color: var(--red); }\n #cmd-suggestions {\n display: none; position: absolute; bottom: 100%; left: 16px; right: 16px;\n background: var(--mantle); border: 1px solid var(--surface1);\n border-bottom: none; border-radius: 8px 8px 0 0;\n overflow: hidden; z-index: 10;\n }\n .cmd-item {\n display: flex; align-items: baseline; gap: 10px;\n padding: 7px 12px; cursor: pointer; font-size: 12px;\n border-bottom: 1px solid var(--surface0);\n }\n .cmd-item:last-child { border-bottom: none; }\n .cmd-item:hover, .cmd-item.active { background: var(--surface0); }\n .cmd-item .cmd-name { color: var(--blue); font-weight: bold; flex-shrink: 0; }\n .cmd-item .cmd-desc { color: var(--subtext0); }\n #input {\n flex: 1; background: var(--surface0); color: var(--text);\n border: none; border-radius: 6px; padding: 8px 12px;\n font-family: inherit; font-size: 13px; resize: none;\n outline: none; line-height: 1.5; min-height: 36px; max-height: 120px;\n }\n #input::placeholder { color: var(--subtext0); }\n #input:focus { box-shadow: 0 0 0 1px var(--mauve); }\n #send {\n background: var(--blue); color: var(--crust); border: none;\n border-radius: 6px; padding: 8px 16px; font-weight: bold;\n cursor: pointer; font-size: 13px; font-family: inherit;\n white-space: nowrap; align-self: flex-end;\n }\n #send:disabled, #input:disabled { opacity: 0.45; cursor: not-allowed; }\n /* While the agent runs, Send becomes a red Stop; the armed (tap-to-confirm)\n state brightens it and adds a halo, mirroring the prompt card's cancel. */\n #send.stop { background: var(--red); color: var(--crust); }\n #send.stop.armed {\n filter: brightness(1.12);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 45%, transparent);\n }\n #reconnect-overlay {\n display: none; position: fixed; inset: 0;\n background: rgba(30,30,46,0.88); color: var(--subtext1);\n justify-content: center; align-items: center;\n font-size: 13px; z-index: 100; letter-spacing: 0.03em;\n }\n #reconnect-overlay.visible { display: flex; }\n /* Trailing stream indicator: the same braille spinner as the thinking bubble\n (one ticker paints every .spin), inline at the end of the streaming text. */\n .cursor {\n color: var(--mauve); margin-left: 2px;\n font-family: ui-monospace, monospace;\n }\n #status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);\n color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }\n /* Structured task widget (progress bar + phase badge + elapsed). Replaces the\n plain text lines when the server sends a structured data payload. */\n #status-panel.structured { white-space: normal; display: block; }\n /* Title gets its own line and wraps, clamped to 2 \u2014 never squeezed to a stub\n by the meta row beside it on a narrow screen. */\n .widget-title { display: -webkit-box; -webkit-box-orient: vertical;\n -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; color: var(--text);\n font-size: 13px; line-height: 1.3; word-break: break-word; }\n .widget-meta { display: flex; align-items: center; gap: 8px; margin-top: 5px; }\n .widget-phase { flex-shrink: 0; background: var(--surface0); color: var(--mauve);\n border-radius: 4px; padding: 1px 7px; font-size: 10px; letter-spacing: 0.03em;\n text-transform: uppercase; }\n .widget-step { flex-shrink: 0; color: var(--subtext0); font-size: 11px;\n font-variant-numeric: tabular-nums; }\n .widget-elapsed { flex-shrink: 0; margin-left: auto; color: var(--subtext0);\n font-size: 11px; font-variant-numeric: tabular-nums; }\n .widget-bar { height: 4px; background: var(--surface0); border-radius: 2px;\n overflow: hidden; margin-top: 6px; }\n /* Fill carries a soft highlight band that sweeps left\u2192right through the mauve,\n so the bar reads as \"working\" even while the step count holds still. The\n gradient tile is 2x the fill width and shifts one full period per cycle,\n so the loop is seamless. Base color stays mauve for reduced-motion. */\n .widget-bar-fill { height: 100%; border-radius: 2px;\n background: linear-gradient(90deg,\n var(--mauve) 0%, var(--mauve) 38%, #ecdcfd 50%,\n var(--mauve) 62%, var(--mauve) 100%);\n background-size: 200% 100%;\n box-shadow: 0 0 6px rgba(203, 166, 247, 0.45);\n animation: bar-flow 2.4s linear infinite;\n transition: width 0.3s ease; }\n @keyframes bar-flow {\n from { background-position: 200% 0; }\n to { background-position: 0% 0; }\n }\n @media (prefers-reduced-motion: reduce) {\n .widget-bar-fill { animation: none; background: var(--mauve); }\n }\n /* Current action \u2014 the terminal's \u21B3 worker trailer, one dim ellipsized line. */\n .widget-action { margin-top: 6px; color: var(--subtext0); font-size: 11px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-variant-numeric: tabular-nums; }\n /* Dim per-turn timestamp shown under a committed turn's bubble. */\n .turn-time { font-size: 10px; color: var(--subtext0); opacity: 0.55; padding: 0 4px;\n margin-top: -2px; }\n .turn-time.user { align-self: flex-end; }\n .turn-time.assistant, .turn-time.system { align-self: flex-start; }\n .turn-time.system { align-self: center; }\n #prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);\n border-top: 2px solid var(--mauve); padding: 16px 14px calc(16px + env(safe-area-inset-bottom, 0px));\n display: none; z-index: 50; max-height: 80dvh; overflow-y: auto; }\n #prompt-card .q-label { color: var(--mauve); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px; }\n #prompt-card .q { color: var(--text); margin-bottom: 12px; white-space: pre-wrap;\n font-size: 15px; line-height: 1.5; }\n #prompt-card .rec-panel { background: var(--surface0); border-left: 3px solid var(--green);\n border-radius: 6px; padding: 10px 12px; margin-bottom: 12px; }\n #prompt-card .rec-label { color: var(--green); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; }\n #prompt-card .rec-text { color: var(--text); font-size: 15px; line-height: 1.5;\n white-space: pre-wrap; overflow-wrap: anywhere; }\n #prompt-card textarea { width: 100%; background: var(--surface0); color: var(--text);\n border: 1px solid var(--surface2); border-radius: 6px; padding: 10px; font-size: 15px;\n font-family: inherit; line-height: 1.5; resize: vertical; margin-bottom: 4px; }\n #prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;\n flex-wrap: wrap; }\n /* Recommendation answers can be long sentences, so stack them as a readable list. */\n #prompt-card .row.stacked { flex-direction: column; align-items: stretch; }\n #prompt-card .row.stacked button { flex: none; text-align: left; }\n #prompt-card .row.stacked button.cancel { align-self: center; text-align: center; }\n #prompt-card button { padding: 11px 16px; border-radius: 8px; border: none; cursor: pointer;\n font-family: inherit; font-size: 14px; font-weight: 600; transition: filter .15s ease; }\n #prompt-card button:hover { filter: brightness(1.08); }\n #prompt-card button.primary { background: var(--green); color: var(--crust);\n font-weight: 700; flex: 1; min-width: 160px; }\n #prompt-card button.secondary { background: var(--surface1); color: var(--text);\n flex: 1; min-width: 160px; }\n #prompt-card button.cancel { margin-left: auto; align-self: center; background: transparent;\n color: var(--subtext0); font-size: 12px; font-weight: 500; padding: 8px 10px; }\n #prompt-card button.cancel:hover { color: var(--red); filter: none; }\n #prompt-card button.cancel.armed { background: var(--red); color: var(--crust); font-weight: 700; }\n .toast { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 12px);\n right: calc(env(safe-area-inset-right, 0px) + 12px); max-width: calc(100vw - 24px);\n padding: 8px 12px; border-radius: 6px; overflow-wrap: anywhere; word-break: break-word;\n background: var(--surface1); color: var(--text); z-index: 60; }\n .toast.warning { background: var(--peach); color: var(--crust); }\n .toast.error { background: var(--red); color: var(--crust); }\n /* Desktop: center the transcript/status/input in a readable column instead of\n hugging the left edge. The scrollbar stays at the true window edge; only the\n content is inset. Mobile (below 960px) is unchanged. */\n @media (min-width: 960px) {\n #chat-log { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #status-panel { padding-left: calc((100% - 920px) / 2 + 12px); padding-right: calc((100% - 920px) / 2 + 12px); }\n #input-bar { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #cmd-suggestions { left: calc((100% - 920px) / 2 + 16px); right: calc((100% - 920px) / 2 + 16px); }\n #held-bar { padding-left: calc((100% - 920px) / 2 + 16px); padding-right: calc((100% - 920px) / 2 + 16px); }\n }";
|