negotium 0.1.12 → 0.1.13
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/canonical-mcp-bridge.js +2 -3
- package/dist/canonical-mcp-bridge.js.map +1 -1
- package/dist/chunk-a9y2rwxf.js.map +1 -1
- package/dist/{chunk-j0kjscb7.js → chunk-s2gez3wg.js} +20 -2
- package/dist/{chunk-j0kjscb7.js.map → chunk-s2gez3wg.js.map} +2 -2
- package/dist/cron.js +49 -25
- package/dist/cron.js.map +15 -14
- package/dist/hosted-agent.js +3012 -17
- package/dist/hosted-agent.js.map +30 -3
- package/dist/main.js +31323 -36
- package/dist/main.js.map +198 -3
- package/dist/prompts.js +11 -8
- package/dist/prompts.js.map +4 -4
- package/dist/runtime/src/application/submit-user-message.ts +3 -0
- package/dist/runtime/src/index.ts +7 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +3 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +1 -1
- package/dist/runtime/src/platform/mcp-config.ts +13 -0
- package/dist/runtime/src/prompts/builders.ts +20 -6
- package/dist/runtime/src/query/active-rooms.ts +2 -0
- package/dist/runtime/src/runtime/delivery-ack.ts +112 -0
- package/dist/runtime/src/runtime/errors.ts +12 -12
- package/dist/runtime/src/runtime/turn-runner.ts +12 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +1 -0
- package/dist/runtime/src/types/api.ts +2 -0
- package/dist/runtime/src/types.ts +2 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js.map +1 -1
- package/dist/storage.js.map +1 -1
- package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +2 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +4 -0
- package/dist/types/packages/core/src/prompts/builders.d.ts +2 -0
- package/dist/types/packages/core/src/types/api.d.ts +2 -0
- package/dist/types/packages/core/src/types.d.ts +2 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-00q25k7p.js +0 -2387
- package/dist/chunk-00q25k7p.js.map +0 -26
- package/dist/chunk-03kv7mvj.js +0 -2152
- package/dist/chunk-03kv7mvj.js.map +0 -15
- package/dist/chunk-05pm51dh.js +0 -589
- package/dist/chunk-05pm51dh.js.map +0 -11
- package/dist/chunk-0eh16bk1.js +0 -2936
- package/dist/chunk-0eh16bk1.js.map +0 -20
- package/dist/chunk-0yfqyv88.js +0 -108
- package/dist/chunk-0yfqyv88.js.map +0 -10
- package/dist/chunk-1k7t7bnx.js +0 -97
- package/dist/chunk-1k7t7bnx.js.map +0 -10
- package/dist/chunk-3gkfr9vb.js +0 -431
- package/dist/chunk-3gkfr9vb.js.map +0 -11
- package/dist/chunk-5akd1gbj.js +0 -281
- package/dist/chunk-5akd1gbj.js.map +0 -10
- package/dist/chunk-7k19pcg8.js +0 -577
- package/dist/chunk-7k19pcg8.js.map +0 -14
- package/dist/chunk-81tbfbmt.js +0 -10
- package/dist/chunk-81tbfbmt.js.map +0 -10
- package/dist/chunk-82qhv1mq.js +0 -512
- package/dist/chunk-82qhv1mq.js.map +0 -13
- package/dist/chunk-8dy7e2a9.js +0 -103
- package/dist/chunk-8dy7e2a9.js.map +0 -10
- package/dist/chunk-8f2sqmrj.js +0 -1712
- package/dist/chunk-8f2sqmrj.js.map +0 -18
- package/dist/chunk-9hjeemfz.js +0 -372
- package/dist/chunk-9hjeemfz.js.map +0 -10
- package/dist/chunk-a4aa2ccv.js +0 -141
- package/dist/chunk-a4aa2ccv.js.map +0 -10
- package/dist/chunk-bsg215q3.js +0 -3536
- package/dist/chunk-bsg215q3.js.map +0 -20
- package/dist/chunk-bxnfsc7w.js +0 -58
- package/dist/chunk-bxnfsc7w.js.map +0 -10
- package/dist/chunk-c500xe9z.js +0 -33
- package/dist/chunk-c500xe9z.js.map +0 -10
- package/dist/chunk-cngrf7ab.js +0 -149
- package/dist/chunk-cngrf7ab.js.map +0 -10
- package/dist/chunk-d7tprf06.js +0 -53
- package/dist/chunk-d7tprf06.js.map +0 -10
- package/dist/chunk-ddnrmkrv.js +0 -61
- package/dist/chunk-ddnrmkrv.js.map +0 -10
- package/dist/chunk-edb07sjp.js +0 -567
- package/dist/chunk-edb07sjp.js.map +0 -14
- package/dist/chunk-ejcm6zx8.js +0 -591
- package/dist/chunk-ejcm6zx8.js.map +0 -11
- package/dist/chunk-ezs352qs.js +0 -237
- package/dist/chunk-ezs352qs.js.map +0 -11
- package/dist/chunk-f5xgprg1.js +0 -59
- package/dist/chunk-f5xgprg1.js.map +0 -10
- package/dist/chunk-fbtb3v0s.js +0 -1339
- package/dist/chunk-fbtb3v0s.js.map +0 -16
- package/dist/chunk-ffefbd3r.js +0 -17
- package/dist/chunk-ffefbd3r.js.map +0 -10
- package/dist/chunk-gaknyw04.js +0 -708
- package/dist/chunk-gaknyw04.js.map +0 -14
- package/dist/chunk-gxmny6tq.js +0 -1637
- package/dist/chunk-gxmny6tq.js.map +0 -15
- package/dist/chunk-h7bj5zwb.js +0 -3069
- package/dist/chunk-h7bj5zwb.js.map +0 -32
- package/dist/chunk-hp4g7wx7.js +0 -46
- package/dist/chunk-hp4g7wx7.js.map +0 -10
- package/dist/chunk-hwy3n31w.js +0 -21
- package/dist/chunk-hwy3n31w.js.map +0 -9
- package/dist/chunk-jzz91jwh.js +0 -70
- package/dist/chunk-jzz91jwh.js.map +0 -10
- package/dist/chunk-k778y16p.js +0 -153
- package/dist/chunk-k778y16p.js.map +0 -11
- package/dist/chunk-mh5mexsf.js +0 -147
- package/dist/chunk-mh5mexsf.js.map +0 -11
- package/dist/chunk-mnwaext5.js +0 -318
- package/dist/chunk-mnwaext5.js.map +0 -10
- package/dist/chunk-nkgs40ch.js +0 -23
- package/dist/chunk-nkgs40ch.js.map +0 -10
- package/dist/chunk-pz57vwsm.js +0 -250
- package/dist/chunk-pz57vwsm.js.map +0 -11
- package/dist/chunk-r6kdp7gn.js +0 -252
- package/dist/chunk-r6kdp7gn.js.map +0 -10
- package/dist/chunk-rkvrkcgx.js +0 -117
- package/dist/chunk-rkvrkcgx.js.map +0 -10
- package/dist/chunk-s74rdyqe.js +0 -117
- package/dist/chunk-s74rdyqe.js.map +0 -12
- package/dist/chunk-sa874j5c.js +0 -9
- package/dist/chunk-sa874j5c.js.map +0 -10
- package/dist/chunk-sffqdkvq.js +0 -122
- package/dist/chunk-sffqdkvq.js.map +0 -10
- package/dist/chunk-tcs9qjps.js +0 -64
- package/dist/chunk-tcs9qjps.js.map +0 -10
- package/dist/chunk-wx83ep8e.js +0 -131
- package/dist/chunk-wx83ep8e.js.map +0 -10
- package/dist/chunk-xag86ng3.js +0 -122
- package/dist/chunk-xag86ng3.js.map +0 -10
- package/dist/chunk-xfdkep71.js +0 -371
- package/dist/chunk-xfdkep71.js.map +0 -11
- package/dist/chunk-xhm1rfpt.js +0 -2907
- package/dist/chunk-xhm1rfpt.js.map +0 -24
- package/dist/chunk-xpcps28p.js +0 -2138
- package/dist/chunk-xpcps28p.js.map +0 -29
- package/dist/chunk-yb4rbev9.js +0 -214
- package/dist/chunk-yb4rbev9.js.map +0 -10
package/dist/chunk-bsg215q3.js
DELETED
|
@@ -1,3536 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
import {
|
|
3
|
-
EmbeddedNegotiumClient,
|
|
4
|
-
INITIAL_MESSAGE_HISTORY_LIMIT,
|
|
5
|
-
MESSAGE_HISTORY_PAGE_SIZE,
|
|
6
|
-
RemoteNegotiumClient
|
|
7
|
-
} from "./chunk-05pm51dh.js";
|
|
8
|
-
import {
|
|
9
|
-
defineNegotiumAdapter
|
|
10
|
-
} from "./chunk-sa874j5c.js";
|
|
11
|
-
import"./chunk-gxmny6tq.js";
|
|
12
|
-
import"./chunk-edb07sjp.js";
|
|
13
|
-
import {
|
|
14
|
-
isVaultCommandLine
|
|
15
|
-
} from "./chunk-h7bj5zwb.js";
|
|
16
|
-
import"./chunk-tcs9qjps.js";
|
|
17
|
-
import"./chunk-7k19pcg8.js";
|
|
18
|
-
import"./chunk-8dy7e2a9.js";
|
|
19
|
-
import"./chunk-9hjeemfz.js";
|
|
20
|
-
import"./chunk-1k7t7bnx.js";
|
|
21
|
-
import {
|
|
22
|
-
getGlobalAiName
|
|
23
|
-
} from "./chunk-xhm1rfpt.js";
|
|
24
|
-
import"./chunk-mnwaext5.js";
|
|
25
|
-
import"./chunk-xpcps28p.js";
|
|
26
|
-
import"./chunk-fbtb3v0s.js";
|
|
27
|
-
import"./chunk-ejcm6zx8.js";
|
|
28
|
-
import"./chunk-hp4g7wx7.js";
|
|
29
|
-
import {
|
|
30
|
-
getRegistry
|
|
31
|
-
} from "./chunk-s74rdyqe.js";
|
|
32
|
-
import"./chunk-k778y16p.js";
|
|
33
|
-
import {
|
|
34
|
-
SELECTABLE_MODELS,
|
|
35
|
-
resolveModelForAgent
|
|
36
|
-
} from "./chunk-xag86ng3.js";
|
|
37
|
-
import"./chunk-wx83ep8e.js";
|
|
38
|
-
import {
|
|
39
|
-
VAULT_DESCRIPTION_MAX_LENGTH,
|
|
40
|
-
VAULT_VALUE_MAX_BYTES,
|
|
41
|
-
VAULT_VALUE_MIN_BYTES,
|
|
42
|
-
normalizeVaultKey,
|
|
43
|
-
validateVaultKey
|
|
44
|
-
} from "./chunk-00q25k7p.js";
|
|
45
|
-
import"./chunk-gaknyw04.js";
|
|
46
|
-
import"./chunk-mh5mexsf.js";
|
|
47
|
-
import {
|
|
48
|
-
EFFORT_VALUES
|
|
49
|
-
} from "./chunk-ezs352qs.js";
|
|
50
|
-
import"./chunk-j0kjscb7.js";
|
|
51
|
-
import"./chunk-82qhv1mq.js";
|
|
52
|
-
import"./chunk-ffefbd3r.js";
|
|
53
|
-
import"./chunk-81tbfbmt.js";
|
|
54
|
-
import"./chunk-sffqdkvq.js";
|
|
55
|
-
import"./chunk-f5xgprg1.js";
|
|
56
|
-
import"./chunk-hwy3n31w.js";
|
|
57
|
-
|
|
58
|
-
// ../../adapters/terminal/src/clipboard.ts
|
|
59
|
-
var MAX_CLIPBOARD_BYTES = 1e5;
|
|
60
|
-
function truncateUtf8(value, maxBytes) {
|
|
61
|
-
if (Buffer.byteLength(value) <= maxBytes)
|
|
62
|
-
return value;
|
|
63
|
-
let output = "";
|
|
64
|
-
let used = 0;
|
|
65
|
-
for (const character of value) {
|
|
66
|
-
const bytes = Buffer.byteLength(character);
|
|
67
|
-
if (used + bytes > maxBytes)
|
|
68
|
-
break;
|
|
69
|
-
output += character;
|
|
70
|
-
used += bytes;
|
|
71
|
-
}
|
|
72
|
-
return output;
|
|
73
|
-
}
|
|
74
|
-
function osc52Sequence(value) {
|
|
75
|
-
const text = truncateUtf8(value, MAX_CLIPBOARD_BYTES);
|
|
76
|
-
return `\x1B]52;c;${Buffer.from(text).toString("base64")}\x07`;
|
|
77
|
-
}
|
|
78
|
-
async function pipeTo(command, args, value) {
|
|
79
|
-
try {
|
|
80
|
-
const child = Bun.spawn([command, ...args], {
|
|
81
|
-
stdin: "pipe",
|
|
82
|
-
stdout: "ignore",
|
|
83
|
-
stderr: "ignore"
|
|
84
|
-
});
|
|
85
|
-
child.stdin.write(value);
|
|
86
|
-
child.stdin.end();
|
|
87
|
-
return await child.exited === 0;
|
|
88
|
-
} catch {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
async function copyToClipboard(value, write = (sequence) => process.stdout.write(sequence)) {
|
|
93
|
-
const text = truncateUtf8(value, MAX_CLIPBOARD_BYTES);
|
|
94
|
-
const truncated = text !== value;
|
|
95
|
-
const nativeCandidates = process.platform === "darwin" ? [["pbcopy", []]] : process.platform === "win32" ? [["clip.exe", []]] : [
|
|
96
|
-
["wl-copy", []],
|
|
97
|
-
["xclip", ["-selection", "clipboard"]],
|
|
98
|
-
["xsel", ["--clipboard", "--input"]],
|
|
99
|
-
["clip.exe", []]
|
|
100
|
-
];
|
|
101
|
-
for (const [command, args] of nativeCandidates) {
|
|
102
|
-
const executable = Bun.which(command);
|
|
103
|
-
if (executable && await pipeTo(executable, args, text)) {
|
|
104
|
-
return { method: "native", truncated };
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const tmux = process.env.TMUX ? Bun.which("tmux") : null;
|
|
108
|
-
if (tmux && await pipeTo(tmux, ["load-buffer", "-"], text)) {
|
|
109
|
-
return { method: "tmux", truncated };
|
|
110
|
-
}
|
|
111
|
-
write(osc52Sequence(text));
|
|
112
|
-
return { method: "osc52", truncated };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ../../adapters/terminal/src/clock.ts
|
|
116
|
-
function terminalNowMs() {
|
|
117
|
-
return performance.timeOrigin + performance.now();
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ../../adapters/terminal/src/commands.ts
|
|
121
|
-
var TERMINAL_COMMANDS = [
|
|
122
|
-
{ name: "new", usage: "/new", description: "reset the current session" },
|
|
123
|
-
{ name: "compact", usage: "/compact", description: "summarize and shrink context" },
|
|
124
|
-
{ name: "status", usage: "/status", description: "show model and token usage" },
|
|
125
|
-
{ name: "model", usage: "/model", description: "choose the model" },
|
|
126
|
-
{ name: "effort", usage: "/effort", description: "choose reasoning effort" },
|
|
127
|
-
{ name: "topics", usage: "/topics", description: "open topic picker" },
|
|
128
|
-
{
|
|
129
|
-
name: "fork",
|
|
130
|
-
usage: "/fork [name]",
|
|
131
|
-
description: "fork this topic (config + history) into a new one"
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
name: "spawn",
|
|
135
|
-
usage: "/spawn [name]",
|
|
136
|
-
description: "spawn a new topic from this one's config (fresh session)"
|
|
137
|
-
},
|
|
138
|
-
{ name: "del", usage: "/del", description: "delete the current topic" },
|
|
139
|
-
{ name: "copy", usage: "/copy [all]", description: "copy answer or transcript" },
|
|
140
|
-
{
|
|
141
|
-
name: "vault",
|
|
142
|
-
usage: "/vault",
|
|
143
|
-
description: "manage encrypted secrets"
|
|
144
|
-
},
|
|
145
|
-
{ name: "abort", usage: "/abort", description: "stop the active turn" },
|
|
146
|
-
{ name: "help", usage: "/help", description: "show keyboard help" },
|
|
147
|
-
{ name: "quit", usage: "/quit", description: "exit Terminal" }
|
|
148
|
-
];
|
|
149
|
-
function commandSuggestions(input) {
|
|
150
|
-
if (!input.startsWith("/") || input.includes(`
|
|
151
|
-
`))
|
|
152
|
-
return [];
|
|
153
|
-
const token = input.slice(1);
|
|
154
|
-
if (token.includes(" "))
|
|
155
|
-
return [];
|
|
156
|
-
const normalized = token.toLowerCase();
|
|
157
|
-
return TERMINAL_COMMANDS.filter((command) => command.name.startsWith(normalized));
|
|
158
|
-
}
|
|
159
|
-
function completeCommand(input, index) {
|
|
160
|
-
const suggestions = commandSuggestions(input);
|
|
161
|
-
if (suggestions.length === 0)
|
|
162
|
-
return null;
|
|
163
|
-
const selected = suggestions[(index + suggestions.length) % suggestions.length];
|
|
164
|
-
return `${selected.usage.includes("<") || selected.usage.includes("[") ? `/${selected.name} ` : `/${selected.name}`}`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ../../adapters/terminal/src/path-suggest.ts
|
|
168
|
-
import { readdirSync, statSync } from "fs";
|
|
169
|
-
import { homedir } from "os";
|
|
170
|
-
import { basename, dirname, join } from "path";
|
|
171
|
-
var MAX_SUGGESTIONS = 8;
|
|
172
|
-
function activeAtToken(lineText, col) {
|
|
173
|
-
const upto = lineText.slice(0, col);
|
|
174
|
-
const match = /(?:^|\s)@([^\s]*)$/.exec(upto);
|
|
175
|
-
if (!match)
|
|
176
|
-
return null;
|
|
177
|
-
const frag = match[1] ?? "";
|
|
178
|
-
return { start: col - frag.length - 1, frag };
|
|
179
|
-
}
|
|
180
|
-
function resolveFragment(frag) {
|
|
181
|
-
const home = homedir();
|
|
182
|
-
let path;
|
|
183
|
-
if (frag === "" || frag === "~")
|
|
184
|
-
path = home;
|
|
185
|
-
else if (frag === "~/")
|
|
186
|
-
path = home;
|
|
187
|
-
else if (frag.startsWith("~/"))
|
|
188
|
-
path = join(home, frag.slice(2));
|
|
189
|
-
else if (frag.startsWith("/"))
|
|
190
|
-
path = frag;
|
|
191
|
-
else
|
|
192
|
-
path = join(home, frag);
|
|
193
|
-
if (frag.endsWith("/") || frag === "" || frag === "~") {
|
|
194
|
-
return { dir: path, prefix: "" };
|
|
195
|
-
}
|
|
196
|
-
return { dir: dirname(path), prefix: basename(path) };
|
|
197
|
-
}
|
|
198
|
-
function toToken(fullPath, isDir) {
|
|
199
|
-
const home = homedir();
|
|
200
|
-
let shown = fullPath;
|
|
201
|
-
if (fullPath === home)
|
|
202
|
-
shown = "~";
|
|
203
|
-
else if (fullPath.startsWith(`${home}/`))
|
|
204
|
-
shown = `~/${fullPath.slice(home.length + 1)}`;
|
|
205
|
-
return `@${shown}${isDir ? "/" : ""}`;
|
|
206
|
-
}
|
|
207
|
-
function pathSuggestions(lineText, col) {
|
|
208
|
-
const token = activeAtToken(lineText, col);
|
|
209
|
-
if (!token)
|
|
210
|
-
return null;
|
|
211
|
-
const { dir, prefix } = resolveFragment(token.frag);
|
|
212
|
-
let entries;
|
|
213
|
-
try {
|
|
214
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
215
|
-
} catch {
|
|
216
|
-
return { start: token.start, end: col, items: [], truncated: 0 };
|
|
217
|
-
}
|
|
218
|
-
const lowerPrefix = prefix.toLowerCase();
|
|
219
|
-
const showHidden = prefix.startsWith(".");
|
|
220
|
-
const matched = entries.filter((entry) => {
|
|
221
|
-
const name = entry.name;
|
|
222
|
-
if (!showHidden && name.startsWith("."))
|
|
223
|
-
return false;
|
|
224
|
-
return name.toLowerCase().startsWith(lowerPrefix);
|
|
225
|
-
}).map((entry) => {
|
|
226
|
-
let isDir = entry.isDirectory();
|
|
227
|
-
if (!isDir && entry.isSymbolicLink()) {
|
|
228
|
-
try {
|
|
229
|
-
isDir = statSync(join(dir, entry.name)).isDirectory();
|
|
230
|
-
} catch {}
|
|
231
|
-
}
|
|
232
|
-
return { name: entry.name, isDir };
|
|
233
|
-
}).sort((a, b) => {
|
|
234
|
-
if (a.isDir !== b.isDir)
|
|
235
|
-
return a.isDir ? -1 : 1;
|
|
236
|
-
return a.name.localeCompare(b.name);
|
|
237
|
-
});
|
|
238
|
-
const items = matched.slice(0, MAX_SUGGESTIONS).map(({ name, isDir }) => ({
|
|
239
|
-
label: `${name}${isDir ? "/" : ""}`,
|
|
240
|
-
value: toToken(join(dir, name), isDir),
|
|
241
|
-
isDir
|
|
242
|
-
}));
|
|
243
|
-
return {
|
|
244
|
-
start: token.start,
|
|
245
|
-
end: col,
|
|
246
|
-
items,
|
|
247
|
-
truncated: Math.max(0, matched.length - items.length)
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
function completePathToken(lineText, col, suggestion, options = {}) {
|
|
251
|
-
const token = activeAtToken(lineText, col);
|
|
252
|
-
if (!token)
|
|
253
|
-
return null;
|
|
254
|
-
const replacement = options.keepTrigger ? suggestion.value : suggestion.value.slice(1);
|
|
255
|
-
const line = lineText.slice(0, token.start) + replacement + lineText.slice(col);
|
|
256
|
-
return { line, col: token.start + replacement.length };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// ../../adapters/terminal/src/state.ts
|
|
260
|
-
function createInitialState(userId) {
|
|
261
|
-
return {
|
|
262
|
-
userId,
|
|
263
|
-
aiName: getGlobalAiName(),
|
|
264
|
-
topics: [],
|
|
265
|
-
backgroundSessions: [],
|
|
266
|
-
activeTopicId: null,
|
|
267
|
-
messages: {},
|
|
268
|
-
messageHistory: {},
|
|
269
|
-
activity: {},
|
|
270
|
-
input: "",
|
|
271
|
-
inputCursor: { row: 0, col: 0 },
|
|
272
|
-
suggestionIndex: 0,
|
|
273
|
-
topicPickerIndex: 0,
|
|
274
|
-
modelPickerIndex: 0,
|
|
275
|
-
effortPickerIndex: 0,
|
|
276
|
-
creatingTopic: false,
|
|
277
|
-
scrollOffset: 0,
|
|
278
|
-
askChoiceIndex: 0,
|
|
279
|
-
overlay: null,
|
|
280
|
-
topicPickerRoot: false,
|
|
281
|
-
vaultEntries: [],
|
|
282
|
-
vaultPickerIndex: 0,
|
|
283
|
-
vaultMode: "list",
|
|
284
|
-
vaultDraftDescription: "",
|
|
285
|
-
vaultEditing: false
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
function activeTopic(state) {
|
|
289
|
-
return state.topics.find((topic) => topic.id === state.activeTopicId) ?? null;
|
|
290
|
-
}
|
|
291
|
-
function activeMessages(state) {
|
|
292
|
-
return state.activeTopicId ? state.messages[state.activeTopicId] ?? [] : [];
|
|
293
|
-
}
|
|
294
|
-
function activeQuestion(state) {
|
|
295
|
-
return activeMessages(state).slice().reverse().find((message) => message.kind === "ask_user_question" && message.askUserQuestion && !message.askUserQuestion.expired && !message.askUserQuestion.selectedLabel) ?? null;
|
|
296
|
-
}
|
|
297
|
-
function activeTaskPanel(state) {
|
|
298
|
-
return activeMessages(state).slice().reverse().find((message) => message.id.startsWith("tasks-")) ?? null;
|
|
299
|
-
}
|
|
300
|
-
function orderTopicsByParent(topics) {
|
|
301
|
-
const topicIds = new Set(topics.map((topic) => topic.id));
|
|
302
|
-
const childrenByParent = new Map;
|
|
303
|
-
const attachedChildIds = new Set;
|
|
304
|
-
for (const topic of topics) {
|
|
305
|
-
if (!topic.isSubagent || !topic.parentTopicId || topic.parentTopicId === topic.id || !topicIds.has(topic.parentTopicId)) {
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
const children = childrenByParent.get(topic.parentTopicId) ?? [];
|
|
309
|
-
children.push(topic);
|
|
310
|
-
childrenByParent.set(topic.parentTopicId, children);
|
|
311
|
-
attachedChildIds.add(topic.id);
|
|
312
|
-
}
|
|
313
|
-
const ordered = [];
|
|
314
|
-
const visited = new Set;
|
|
315
|
-
const appendTopic = (topic) => {
|
|
316
|
-
if (visited.has(topic.id))
|
|
317
|
-
return;
|
|
318
|
-
visited.add(topic.id);
|
|
319
|
-
ordered.push(topic);
|
|
320
|
-
for (const child of childrenByParent.get(topic.id) ?? [])
|
|
321
|
-
appendTopic(child);
|
|
322
|
-
};
|
|
323
|
-
const roots = topics.filter((topic) => !attachedChildIds.has(topic.id));
|
|
324
|
-
const orderedRoots = [
|
|
325
|
-
...roots.filter((topic) => topic.title.toLowerCase() === "general"),
|
|
326
|
-
...roots.filter((topic) => topic.title.toLowerCase() !== "general")
|
|
327
|
-
];
|
|
328
|
-
for (const topic of orderedRoots) {
|
|
329
|
-
if (!attachedChildIds.has(topic.id))
|
|
330
|
-
appendTopic(topic);
|
|
331
|
-
}
|
|
332
|
-
for (const topic of topics)
|
|
333
|
-
appendTopic(topic);
|
|
334
|
-
return ordered;
|
|
335
|
-
}
|
|
336
|
-
function setBackgroundSessions(state, backgroundSessions) {
|
|
337
|
-
const orderedSessions = [
|
|
338
|
-
...backgroundSessions.filter((session) => session.kind === "memory"),
|
|
339
|
-
...backgroundSessions.filter((session) => session.kind === "cron")
|
|
340
|
-
];
|
|
341
|
-
const selectedStillExists = orderedSessions.some((session) => session.id === state.topicPickerBackgroundId);
|
|
342
|
-
return {
|
|
343
|
-
...state,
|
|
344
|
-
backgroundSessions: orderedSessions,
|
|
345
|
-
topicPickerBackgroundId: selectedStillExists ? state.topicPickerBackgroundId : undefined,
|
|
346
|
-
overlay: state.overlay === "background-session" && !selectedStillExists ? "topics" : state.overlay
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
function pickedBackgroundSession(state) {
|
|
350
|
-
return state.backgroundSessions.find((session) => session.id === state.topicPickerBackgroundId);
|
|
351
|
-
}
|
|
352
|
-
function pickedTopic(state) {
|
|
353
|
-
return state.topicPickerBackgroundId ? undefined : state.topics[state.topicPickerIndex];
|
|
354
|
-
}
|
|
355
|
-
function moveTopicPickerSelection(state, delta) {
|
|
356
|
-
const items = [
|
|
357
|
-
...state.topics.map((topic, index) => ({ kind: "topic", id: topic.id, index })),
|
|
358
|
-
...state.backgroundSessions.map((session) => ({
|
|
359
|
-
kind: "background",
|
|
360
|
-
id: session.id
|
|
361
|
-
}))
|
|
362
|
-
];
|
|
363
|
-
if (items.length === 0)
|
|
364
|
-
return state;
|
|
365
|
-
const currentId = state.topicPickerBackgroundId ?? state.topics[state.topicPickerIndex]?.id;
|
|
366
|
-
const current = Math.max(0, items.findIndex((item) => item.id === currentId));
|
|
367
|
-
const next = items[(current + delta + items.length) % items.length];
|
|
368
|
-
return next.kind === "topic" ? { ...state, topicPickerIndex: next.index, topicPickerBackgroundId: undefined } : { ...state, topicPickerBackgroundId: next.id };
|
|
369
|
-
}
|
|
370
|
-
function setTopics(state, topics, preferredTitle) {
|
|
371
|
-
const orderedTopics = orderTopicsByParent(topics);
|
|
372
|
-
const stillVisible = orderedTopics.some((topic) => topic.id === state.activeTopicId);
|
|
373
|
-
let activity = state.activity;
|
|
374
|
-
for (const topic of orderedTopics) {
|
|
375
|
-
const current = activity[topic.id];
|
|
376
|
-
const snapshotIsNewer = topic.runningQueryId && current?.running === false && current.queryId !== topic.runningQueryId;
|
|
377
|
-
if (topic.running && (!current || current.snapshot || snapshotIsNewer)) {
|
|
378
|
-
if (activity === state.activity)
|
|
379
|
-
activity = { ...state.activity };
|
|
380
|
-
activity[topic.id] = {
|
|
381
|
-
running: true,
|
|
382
|
-
queryId: topic.runningQueryId,
|
|
383
|
-
snapshot: true,
|
|
384
|
-
tools: [],
|
|
385
|
-
startedAtMs: current?.snapshot && current.queryId === topic.runningQueryId ? current.startedAtMs : Date.now()
|
|
386
|
-
};
|
|
387
|
-
} else if (!topic.running && current?.snapshot) {
|
|
388
|
-
if (activity === state.activity)
|
|
389
|
-
activity = { ...state.activity };
|
|
390
|
-
delete activity[topic.id];
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
const pickedTopicId = state.topics[state.topicPickerIndex]?.id;
|
|
394
|
-
const pickedTopicIndex = orderedTopics.findIndex((topic) => topic.id === pickedTopicId);
|
|
395
|
-
const preferred = preferredTitle ? orderedTopics.find((topic) => topic.title.toLowerCase() === preferredTitle.toLowerCase()) : undefined;
|
|
396
|
-
const nextActive = state.topicPickerRoot ? null : preferred?.id ?? (stillVisible ? state.activeTopicId : orderedTopics[0]?.id) ?? null;
|
|
397
|
-
return {
|
|
398
|
-
...state,
|
|
399
|
-
topics: orderedTopics,
|
|
400
|
-
activity,
|
|
401
|
-
activeTopicId: nextActive,
|
|
402
|
-
scrollOffset: nextActive === state.activeTopicId ? state.scrollOffset : 0,
|
|
403
|
-
askChoiceIndex: nextActive === state.activeTopicId ? state.askChoiceIndex : 0,
|
|
404
|
-
topicPickerIndex: state.overlay === "topics" ? Math.max(0, pickedTopicIndex) : Math.max(0, orderedTopics.findIndex((topic) => topic.id === nextActive)),
|
|
405
|
-
topicPickerBackgroundId: state.topicPickerBackgroundId
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
function selectTopic(state, topicId) {
|
|
409
|
-
if (!state.topics.some((topic) => topic.id === topicId))
|
|
410
|
-
return state;
|
|
411
|
-
return {
|
|
412
|
-
...state,
|
|
413
|
-
activeTopicId: topicId,
|
|
414
|
-
scrollOffset: 0,
|
|
415
|
-
askChoiceIndex: 0,
|
|
416
|
-
overlay: null,
|
|
417
|
-
topicPickerRoot: false,
|
|
418
|
-
creatingTopic: false,
|
|
419
|
-
topicPickerIndex: state.topics.findIndex((topic) => topic.id === topicId),
|
|
420
|
-
topicPickerBackgroundId: undefined,
|
|
421
|
-
notice: undefined
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
function focusCreatedTopic(state, topic) {
|
|
425
|
-
const topics = state.topics.some((candidate) => candidate.id === topic.id) ? state.topics.map((candidate) => candidate.id === topic.id ? topic : candidate) : [...state.topics, topic];
|
|
426
|
-
return selectTopic(setTopics(state, topics), topic.id);
|
|
427
|
-
}
|
|
428
|
-
function openTopicPicker(state, notice = state.notice, topicPickerRoot = false) {
|
|
429
|
-
const activeIndex = state.topics.findIndex((topic) => topic.id === state.activeTopicId);
|
|
430
|
-
return {
|
|
431
|
-
...state,
|
|
432
|
-
activeTopicId: topicPickerRoot ? null : state.activeTopicId,
|
|
433
|
-
overlay: "topics",
|
|
434
|
-
topicPickerRoot,
|
|
435
|
-
creatingTopic: false,
|
|
436
|
-
topicPickerIndex: Math.max(0, activeIndex >= 0 ? activeIndex : state.topicPickerIndex),
|
|
437
|
-
topicPickerBackgroundId: undefined,
|
|
438
|
-
notice
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
function startTopicCreation(state) {
|
|
442
|
-
return {
|
|
443
|
-
...state,
|
|
444
|
-
overlay: null,
|
|
445
|
-
creatingTopic: true,
|
|
446
|
-
notice: "Type a new topic name, then press Enter"
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
function setMessages(state, topicId, messages) {
|
|
450
|
-
return { ...state, messages: { ...state.messages, [topicId]: messages } };
|
|
451
|
-
}
|
|
452
|
-
function setMessageHistoryStatus(state, topicId, status) {
|
|
453
|
-
return {
|
|
454
|
-
...state,
|
|
455
|
-
messageHistory: { ...state.messageHistory, [topicId]: status }
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
function upsertMessage(state, message) {
|
|
459
|
-
const current = state.messages[message.topicId] ?? [];
|
|
460
|
-
const index = current.findIndex((item) => item.id === message.id);
|
|
461
|
-
const next = [...current];
|
|
462
|
-
if (index >= 0)
|
|
463
|
-
next[index] = message;
|
|
464
|
-
else
|
|
465
|
-
next.push(message);
|
|
466
|
-
next.sort((left, right) => {
|
|
467
|
-
const leftTime = Date.parse(left.createdAt);
|
|
468
|
-
const rightTime = Date.parse(right.createdAt);
|
|
469
|
-
if (!Number.isFinite(leftTime) || !Number.isFinite(rightTime))
|
|
470
|
-
return 0;
|
|
471
|
-
return leftTime - rightTime;
|
|
472
|
-
});
|
|
473
|
-
return {
|
|
474
|
-
...state,
|
|
475
|
-
messages: { ...state.messages, [message.topicId]: next },
|
|
476
|
-
scrollOffset: state.scrollOffset
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
function patchMessage(state, topicId, messageId, patch) {
|
|
480
|
-
const current = state.messages[topicId] ?? [];
|
|
481
|
-
const index = current.findIndex((message) => message.id === messageId);
|
|
482
|
-
if (index < 0)
|
|
483
|
-
return state;
|
|
484
|
-
const next = [...current];
|
|
485
|
-
next[index] = { ...next[index], ...patch };
|
|
486
|
-
return { ...state, messages: { ...state.messages, [topicId]: next } };
|
|
487
|
-
}
|
|
488
|
-
function removeMessage(state, topicId, messageId) {
|
|
489
|
-
const current = state.messages[topicId] ?? [];
|
|
490
|
-
const next = current.filter((message) => message.id !== messageId);
|
|
491
|
-
if (next.length === current.length)
|
|
492
|
-
return state;
|
|
493
|
-
return { ...state, messages: { ...state.messages, [topicId]: next } };
|
|
494
|
-
}
|
|
495
|
-
function activityFor(state, topicId) {
|
|
496
|
-
return state.activity[topicId] ?? { running: false, tools: [] };
|
|
497
|
-
}
|
|
498
|
-
function setActivity(state, topicId, activity) {
|
|
499
|
-
return { ...state, activity: { ...state.activity, [topicId]: activity } };
|
|
500
|
-
}
|
|
501
|
-
function activityStartMs(createdAt) {
|
|
502
|
-
const parsed = createdAt ? Date.parse(createdAt) : Number.NaN;
|
|
503
|
-
return Number.isFinite(parsed) ? parsed : terminalNowMs();
|
|
504
|
-
}
|
|
505
|
-
function isStaleTerminalStatus(current, status) {
|
|
506
|
-
const queryId = typeof status.queryId === "string" ? status.queryId.trim() : "";
|
|
507
|
-
return Boolean(queryId && current.queryId && queryId !== current.queryId);
|
|
508
|
-
}
|
|
509
|
-
function compactPath(value) {
|
|
510
|
-
if (typeof value !== "string" || !value.trim())
|
|
511
|
-
return "file";
|
|
512
|
-
const normalized = value.trim().replaceAll("\\", "/");
|
|
513
|
-
const parts = normalized.split("/").filter(Boolean);
|
|
514
|
-
return parts.length > 2 ? `\u2026/${parts.slice(-2).join("/")}` : normalized;
|
|
515
|
-
}
|
|
516
|
-
function compactToolLabel(value) {
|
|
517
|
-
const match = value.match(/^([^()]+)\((.*)\)$/);
|
|
518
|
-
return match ? `${match[1]} \xB7 ${match[2]}` : value;
|
|
519
|
-
}
|
|
520
|
-
function toolTimelineText(status) {
|
|
521
|
-
const name = String(status.name ?? "tool");
|
|
522
|
-
const shortName = name.split("__").at(-1)?.toLowerCase() ?? name.toLowerCase();
|
|
523
|
-
const input = status.input && typeof status.input === "object" ? status.input : {};
|
|
524
|
-
const path = compactPath(input.file_path ?? input.path ?? input.file_id);
|
|
525
|
-
if (shortName === "ask_session" || shortName === "tell_session") {
|
|
526
|
-
const target = typeof input.to === "string" && input.to.trim() ? input.to.trim() : "session";
|
|
527
|
-
const message = typeof input.message === "string" ? input.message.trim() : "";
|
|
528
|
-
const action = shortName === "ask_session" ? "Ask session" : "Tell session";
|
|
529
|
-
return [`${action} \xB7 ${target}`, message].filter(Boolean).join(`
|
|
530
|
-
`);
|
|
531
|
-
}
|
|
532
|
-
if (shortName === "edit") {
|
|
533
|
-
const before = typeof input.before === "string" ? input.before : "";
|
|
534
|
-
const after = typeof input.after === "string" ? input.after : "";
|
|
535
|
-
return [`Edit \xB7 ${path}`, before ? `- ${before}` : "", after ? `+ ${after}` : ""].filter(Boolean).join(`
|
|
536
|
-
`);
|
|
537
|
-
}
|
|
538
|
-
if (shortName === "write") {
|
|
539
|
-
const lines = typeof input.lines === "number" ? ` \xB7 ${input.lines} lines` : "";
|
|
540
|
-
const preview = typeof input.preview === "string" ? input.preview : "";
|
|
541
|
-
return [`Write \xB7 ${path}${lines}`, preview ? `+ ${preview}` : ""].filter(Boolean).join(`
|
|
542
|
-
`);
|
|
543
|
-
}
|
|
544
|
-
return compactToolLabel(String(status.label ?? status.name ?? "tool"));
|
|
545
|
-
}
|
|
546
|
-
function toolMessageId(status, toolUseId) {
|
|
547
|
-
return `terminal-tool:${String(status.queryId ?? "query")}:${toolUseId}`;
|
|
548
|
-
}
|
|
549
|
-
function applyAiStatus(state, topicId, raw, createdAt) {
|
|
550
|
-
const status = raw ?? {};
|
|
551
|
-
const kind = String(status.kind ?? "");
|
|
552
|
-
const current = activityFor(state, topicId);
|
|
553
|
-
const liveCurrent = { ...current };
|
|
554
|
-
delete liveCurrent.snapshot;
|
|
555
|
-
if (kind === "ai_active") {
|
|
556
|
-
const queryId = String(status.queryId ?? "");
|
|
557
|
-
return setActivity(state, topicId, {
|
|
558
|
-
running: true,
|
|
559
|
-
queryId,
|
|
560
|
-
startedAtMs: current.running && current.queryId === queryId && current.startedAtMs !== undefined ? current.startedAtMs : activityStartMs(createdAt),
|
|
561
|
-
status: "Thinking\u2026",
|
|
562
|
-
tools: []
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
if (kind === "ai_done") {
|
|
566
|
-
if (isStaleTerminalStatus(current, status))
|
|
567
|
-
return state;
|
|
568
|
-
return setActivity(state, topicId, {
|
|
569
|
-
...liveCurrent,
|
|
570
|
-
running: false,
|
|
571
|
-
status: "Done"
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
if (kind === "ai_aborted") {
|
|
575
|
-
if (isStaleTerminalStatus(current, status))
|
|
576
|
-
return state;
|
|
577
|
-
return setActivity(state, topicId, {
|
|
578
|
-
...liveCurrent,
|
|
579
|
-
running: false,
|
|
580
|
-
status: "Aborted"
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
if (kind === "ai_error") {
|
|
584
|
-
if (isStaleTerminalStatus(current, status))
|
|
585
|
-
return state;
|
|
586
|
-
return setActivity(state, topicId, {
|
|
587
|
-
...liveCurrent,
|
|
588
|
-
running: false,
|
|
589
|
-
status: "Error",
|
|
590
|
-
error: String(status.error ?? "Unknown error")
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
if (kind === "tool_call") {
|
|
594
|
-
if (isStaleTerminalStatus(current, status))
|
|
595
|
-
return state;
|
|
596
|
-
const queryId = String(status.queryId ?? "");
|
|
597
|
-
const tool = {
|
|
598
|
-
id: String(status.toolUseId ?? `${queryId || "query"}:tool`),
|
|
599
|
-
label: String(status.label ?? status.name ?? "tool"),
|
|
600
|
-
status: "running"
|
|
601
|
-
};
|
|
602
|
-
const withActivity = setActivity(state, topicId, {
|
|
603
|
-
...liveCurrent,
|
|
604
|
-
running: true,
|
|
605
|
-
queryId: queryId || current.queryId,
|
|
606
|
-
startedAtMs: current.queryId === queryId && current.startedAtMs !== undefined ? current.startedAtMs : activityStartMs(createdAt),
|
|
607
|
-
status: tool.label,
|
|
608
|
-
tools: [...current.tools.filter((item) => item.id !== tool.id), tool].slice(-8)
|
|
609
|
-
});
|
|
610
|
-
return upsertMessage(withActivity, {
|
|
611
|
-
id: toolMessageId(status, tool.id),
|
|
612
|
-
topicId,
|
|
613
|
-
authorId: "ai",
|
|
614
|
-
text: toolTimelineText(status),
|
|
615
|
-
kind: "tool",
|
|
616
|
-
queryId: typeof status.queryId === "string" ? status.queryId : undefined,
|
|
617
|
-
createdAt: createdAt ?? new Date().toISOString()
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
if (kind === "tool_output") {
|
|
621
|
-
if (isStaleTerminalStatus(current, status))
|
|
622
|
-
return state;
|
|
623
|
-
const id = String(status.toolUseId ?? "");
|
|
624
|
-
const tools = current.tools.map((tool) => tool.id === id ? { ...tool, output: String(status.content ?? ""), status: "done" } : tool);
|
|
625
|
-
const withActivity = setActivity(state, topicId, { ...liveCurrent, tools });
|
|
626
|
-
return patchMessage(withActivity, topicId, toolMessageId(status, id), {
|
|
627
|
-
editedAt: createdAt ?? new Date().toISOString()
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
if (kind === "tool_status") {
|
|
631
|
-
if (isStaleTerminalStatus(current, status))
|
|
632
|
-
return state;
|
|
633
|
-
return setActivity(state, topicId, {
|
|
634
|
-
...liveCurrent,
|
|
635
|
-
status: String(status.content ?? current.status ?? "Working\u2026")
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
return state;
|
|
639
|
-
}
|
|
640
|
-
function applyRuntimeEvent(state, event) {
|
|
641
|
-
if (event.type === "message") {
|
|
642
|
-
return upsertMessage(state, event.payload);
|
|
643
|
-
}
|
|
644
|
-
if (event.type === "message-updated") {
|
|
645
|
-
const payload = event.payload;
|
|
646
|
-
if (!payload.messageId || !payload.patch)
|
|
647
|
-
return state;
|
|
648
|
-
if (payload.patch.deleted)
|
|
649
|
-
return removeMessage(state, event.topicId, payload.messageId);
|
|
650
|
-
return patchMessage(state, event.topicId, payload.messageId, payload.patch);
|
|
651
|
-
}
|
|
652
|
-
if (event.type === "ai-status") {
|
|
653
|
-
return applyAiStatus(state, event.topicId, event.payload, event.createdAt);
|
|
654
|
-
}
|
|
655
|
-
if (event.type === "topic-deleted" && state.activeTopicId === event.topicId) {
|
|
656
|
-
return { ...state, activeTopicId: null };
|
|
657
|
-
}
|
|
658
|
-
return state;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// ../../adapters/terminal/src/render.ts
|
|
662
|
-
var theme = {
|
|
663
|
-
canvas: [10, 11, 15],
|
|
664
|
-
surface: [18, 20, 27],
|
|
665
|
-
surfaceRaised: [24, 27, 36],
|
|
666
|
-
selected: [42, 37, 69],
|
|
667
|
-
border: [48, 52, 67],
|
|
668
|
-
borderActive: [119, 103, 239],
|
|
669
|
-
text: [232, 233, 239],
|
|
670
|
-
muted: [137, 141, 158],
|
|
671
|
-
subtle: [91, 95, 112],
|
|
672
|
-
accent: [139, 124, 246],
|
|
673
|
-
cyan: [87, 205, 220],
|
|
674
|
-
green: [94, 211, 142],
|
|
675
|
-
amber: [241, 190, 91],
|
|
676
|
-
red: [245, 116, 128]
|
|
677
|
-
};
|
|
678
|
-
var WORKING_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
679
|
-
var WORKING_FRAME_INTERVAL_MS = 50;
|
|
680
|
-
function workingFrame(frame) {
|
|
681
|
-
const index = Math.abs(Math.trunc(frame)) % WORKING_FRAMES.length;
|
|
682
|
-
return WORKING_FRAMES[index] ?? WORKING_FRAMES[0];
|
|
683
|
-
}
|
|
684
|
-
function workingElapsedSeconds(startedAtMs, nowMs = terminalNowMs()) {
|
|
685
|
-
if (startedAtMs === undefined || !Number.isFinite(startedAtMs))
|
|
686
|
-
return 0;
|
|
687
|
-
return Math.max(0, Math.floor((nowMs - startedAtMs) / 1000));
|
|
688
|
-
}
|
|
689
|
-
function formatElapsedDuration(totalSeconds) {
|
|
690
|
-
const elapsed = Number.isFinite(totalSeconds) ? Math.max(0, Math.floor(totalSeconds)) : 0;
|
|
691
|
-
const days = Math.floor(elapsed / 86400);
|
|
692
|
-
const hours = Math.floor(elapsed % 86400 / 3600);
|
|
693
|
-
const minutes = Math.floor(elapsed % 3600 / 60);
|
|
694
|
-
const seconds = elapsed % 60;
|
|
695
|
-
const parts = [];
|
|
696
|
-
if (days > 0)
|
|
697
|
-
parts.push(`${days}d`);
|
|
698
|
-
if (days > 0 || hours > 0)
|
|
699
|
-
parts.push(`${hours}h`);
|
|
700
|
-
if (days > 0 || hours > 0 || minutes > 0)
|
|
701
|
-
parts.push(`${minutes}m`);
|
|
702
|
-
parts.push(`${seconds}s`);
|
|
703
|
-
return parts.join(" ");
|
|
704
|
-
}
|
|
705
|
-
function effectiveTopicModel(topic) {
|
|
706
|
-
if (!topic?.agent)
|
|
707
|
-
return topic?.defaultModel ?? "-";
|
|
708
|
-
return resolveModelForAgent(topic.agent, topic.effectiveModel ?? topic.defaultModel, getRegistry(topic.agent));
|
|
709
|
-
}
|
|
710
|
-
function effectiveTopicEffort(topic) {
|
|
711
|
-
return topic?.effectiveEffort ?? topic?.defaultEffort ?? "-";
|
|
712
|
-
}
|
|
713
|
-
var ESC = "\x1B[";
|
|
714
|
-
var RESET = `${ESC}0m`;
|
|
715
|
-
var fg = ([r, g, b]) => `${ESC}38;2;${r};${g};${b}m`;
|
|
716
|
-
var bg = ([r, g, b]) => `${ESC}48;2;${r};${g};${b}m`;
|
|
717
|
-
function paint(value, options = {}) {
|
|
718
|
-
return `${options.fg ? fg(options.fg) : ""}${options.bg ? bg(options.bg) : ""}${options.bold ? `${ESC}1m` : ""}${options.dim ? `${ESC}2m` : ""}${value}${RESET}`;
|
|
719
|
-
}
|
|
720
|
-
function stripAnsi(value) {
|
|
721
|
-
return value.replace(new RegExp("\\u001b\\[[0-?]*[ -/]*[@-~]", "g"), "");
|
|
722
|
-
}
|
|
723
|
-
function safeText(value) {
|
|
724
|
-
return [...stripAnsi(value).replaceAll("\r", "")].filter((character) => {
|
|
725
|
-
const code = character.charCodeAt(0);
|
|
726
|
-
return code === 9 || code === 10 || code >= 32 && code !== 127;
|
|
727
|
-
}).join("");
|
|
728
|
-
}
|
|
729
|
-
function runeWidth(char) {
|
|
730
|
-
const code = char.codePointAt(0) ?? 0;
|
|
731
|
-
if (code === 8205 || code >= 65024 && code <= 65039)
|
|
732
|
-
return 0;
|
|
733
|
-
if (code >= 4352 && (code <= 4447 || code === 9001 || code === 9002 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 127744 && code <= 129791 || code >= 131072)) {
|
|
734
|
-
return 2;
|
|
735
|
-
}
|
|
736
|
-
return 1;
|
|
737
|
-
}
|
|
738
|
-
function displayWidth(value) {
|
|
739
|
-
return [...stripAnsi(value)].reduce((width, char) => width + runeWidth(char), 0);
|
|
740
|
-
}
|
|
741
|
-
function sliceWidth(value, width) {
|
|
742
|
-
let out = "";
|
|
743
|
-
let used = 0;
|
|
744
|
-
for (const char of [...safeText(value)]) {
|
|
745
|
-
const next = runeWidth(char);
|
|
746
|
-
if (used + next > width)
|
|
747
|
-
break;
|
|
748
|
-
out += char;
|
|
749
|
-
used += next;
|
|
750
|
-
}
|
|
751
|
-
return out;
|
|
752
|
-
}
|
|
753
|
-
function fit(value, width) {
|
|
754
|
-
if (width <= 0)
|
|
755
|
-
return "";
|
|
756
|
-
const clean = safeText(value).replaceAll(`
|
|
757
|
-
`, " ");
|
|
758
|
-
const clipped = sliceWidth(clean, width);
|
|
759
|
-
return clipped + " ".repeat(Math.max(0, width - displayWidth(clipped)));
|
|
760
|
-
}
|
|
761
|
-
function joinSides(left, right, width) {
|
|
762
|
-
const safeLeft = sliceWidth(left, width);
|
|
763
|
-
const remaining = Math.max(1, width - displayWidth(safeLeft));
|
|
764
|
-
const safeRight = sliceWidth(right, remaining);
|
|
765
|
-
const gap = Math.max(1, width - displayWidth(safeLeft) - displayWidth(safeRight));
|
|
766
|
-
return fit(`${safeLeft}${" ".repeat(gap)}${safeRight}`, width);
|
|
767
|
-
}
|
|
768
|
-
function wrapText(value, width) {
|
|
769
|
-
if (width <= 1)
|
|
770
|
-
return [sliceWidth(value, Math.max(0, width))];
|
|
771
|
-
const output = [];
|
|
772
|
-
for (const paragraph of safeText(value).split(`
|
|
773
|
-
`)) {
|
|
774
|
-
if (!paragraph) {
|
|
775
|
-
output.push("");
|
|
776
|
-
continue;
|
|
777
|
-
}
|
|
778
|
-
let current = "";
|
|
779
|
-
let currentWidth = 0;
|
|
780
|
-
for (const character of [...paragraph]) {
|
|
781
|
-
const characterWidth = runeWidth(character);
|
|
782
|
-
if (currentWidth + characterWidth > width && current) {
|
|
783
|
-
output.push(current);
|
|
784
|
-
current = "";
|
|
785
|
-
currentWidth = 0;
|
|
786
|
-
}
|
|
787
|
-
current += character;
|
|
788
|
-
currentWidth += characterWidth;
|
|
789
|
-
}
|
|
790
|
-
output.push(current);
|
|
791
|
-
}
|
|
792
|
-
return output.length > 0 ? output : [""];
|
|
793
|
-
}
|
|
794
|
-
function line(text, options = {}) {
|
|
795
|
-
return { text, ...options };
|
|
796
|
-
}
|
|
797
|
-
function framePane(title, content, width, height, options = {}) {
|
|
798
|
-
const innerWidth = Math.max(1, width - 2);
|
|
799
|
-
const borderColor = options.active ? options.accent ?? theme.borderActive : theme.border;
|
|
800
|
-
const label = ` ${sliceWidth(title, Math.max(0, innerWidth - 3))} `;
|
|
801
|
-
const top = paint(`\u256D${label}${"\u2500".repeat(Math.max(0, innerWidth - displayWidth(label)))}\u256E`, {
|
|
802
|
-
fg: borderColor,
|
|
803
|
-
bg: theme.canvas,
|
|
804
|
-
bold: true
|
|
805
|
-
});
|
|
806
|
-
const body = Array.from({ length: Math.max(0, height - 2) }, (_, index) => {
|
|
807
|
-
const item = content[index] ?? line("");
|
|
808
|
-
const side = paint("\u2502", { fg: borderColor, bg: theme.canvas });
|
|
809
|
-
const cell = paint(fit(item.text, innerWidth), {
|
|
810
|
-
fg: item.fg ?? theme.text,
|
|
811
|
-
bg: item.bg ?? theme.surface,
|
|
812
|
-
bold: item.bold,
|
|
813
|
-
dim: item.dim
|
|
814
|
-
});
|
|
815
|
-
return `${side}${cell}${side}`;
|
|
816
|
-
});
|
|
817
|
-
const bottom = paint(`\u2570${"\u2500".repeat(innerWidth)}\u256F`, {
|
|
818
|
-
fg: borderColor,
|
|
819
|
-
bg: theme.canvas
|
|
820
|
-
});
|
|
821
|
-
return [top, ...body, bottom];
|
|
822
|
-
}
|
|
823
|
-
function cleanInlineMarkdown(value) {
|
|
824
|
-
return value.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(?<!\*)\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "\u2039$1\u203A");
|
|
825
|
-
}
|
|
826
|
-
function renderMarkdown(value, width) {
|
|
827
|
-
const result = [];
|
|
828
|
-
let codeLanguage = "";
|
|
829
|
-
let inCode = false;
|
|
830
|
-
for (const rawLine of safeText(value).split(`
|
|
831
|
-
`)) {
|
|
832
|
-
const fence = rawLine.match(/^\s*```([^`]*)$/);
|
|
833
|
-
if (fence) {
|
|
834
|
-
if (!inCode) {
|
|
835
|
-
codeLanguage = fence[1]?.trim() ?? "";
|
|
836
|
-
result.push(line(` \u250C\u2500 code${codeLanguage ? ` \xB7 ${codeLanguage}` : ""}`, {
|
|
837
|
-
fg: theme.cyan,
|
|
838
|
-
bg: theme.surfaceRaised,
|
|
839
|
-
bold: true
|
|
840
|
-
}));
|
|
841
|
-
} else {
|
|
842
|
-
result.push(line(" \u2514\u2500", { fg: theme.subtle, bg: theme.surfaceRaised }));
|
|
843
|
-
}
|
|
844
|
-
inCode = !inCode;
|
|
845
|
-
continue;
|
|
846
|
-
}
|
|
847
|
-
if (inCode) {
|
|
848
|
-
for (const wrapped of wrapText(rawLine || " ", Math.max(4, width - 4))) {
|
|
849
|
-
result.push(line(` \u2502 ${wrapped}`, { fg: theme.text, bg: theme.surfaceRaised }));
|
|
850
|
-
}
|
|
851
|
-
continue;
|
|
852
|
-
}
|
|
853
|
-
const heading = rawLine.match(/^\s{0,3}(#{1,6})\s+(.+)$/);
|
|
854
|
-
if (heading) {
|
|
855
|
-
for (const wrapped of wrapText(cleanInlineMarkdown(heading[2]), Math.max(4, width - 2))) {
|
|
856
|
-
result.push(line(` ${wrapped}`, { fg: theme.accent, bold: true }));
|
|
857
|
-
}
|
|
858
|
-
continue;
|
|
859
|
-
}
|
|
860
|
-
const bullet = rawLine.match(/^\s*[-+*]\s+(.+)$/);
|
|
861
|
-
if (bullet) {
|
|
862
|
-
for (const [index, wrapped] of wrapText(cleanInlineMarkdown(bullet[1]), Math.max(4, width - 5)).entries()) {
|
|
863
|
-
result.push(line(` ${index === 0 ? "\u2022" : " "} ${wrapped}`, { fg: theme.text }));
|
|
864
|
-
}
|
|
865
|
-
continue;
|
|
866
|
-
}
|
|
867
|
-
const ordered = rawLine.match(/^\s*(\d+[.)])\s+(.+)$/);
|
|
868
|
-
if (ordered) {
|
|
869
|
-
const marker = ordered[1];
|
|
870
|
-
for (const [index, wrapped] of wrapText(cleanInlineMarkdown(ordered[2]), Math.max(4, width - marker.length - 4)).entries()) {
|
|
871
|
-
result.push(line(` ${index === 0 ? marker : " ".repeat(marker.length)} ${wrapped}`));
|
|
872
|
-
}
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
const quote = rawLine.match(/^\s*>\s?(.*)$/);
|
|
876
|
-
if (quote) {
|
|
877
|
-
for (const wrapped of wrapText(cleanInlineMarkdown(quote[1]), Math.max(4, width - 5))) {
|
|
878
|
-
result.push(line(` \u258F ${wrapped}`, { fg: theme.muted }));
|
|
879
|
-
}
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
if (/^\s*([-*_])(?:\s*\1){2,}\s*$/.test(rawLine)) {
|
|
883
|
-
result.push(line(` ${"\u2500".repeat(Math.max(1, width - 4))}`, { fg: theme.border }));
|
|
884
|
-
continue;
|
|
885
|
-
}
|
|
886
|
-
if (!rawLine.trim()) {
|
|
887
|
-
result.push(line(""));
|
|
888
|
-
continue;
|
|
889
|
-
}
|
|
890
|
-
for (const wrapped of wrapText(cleanInlineMarkdown(rawLine), Math.max(4, width - 2))) {
|
|
891
|
-
result.push(line(` ${wrapped}`, { fg: theme.text }));
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
if (inCode)
|
|
895
|
-
result.push(line(" \u2514\u2500", { fg: theme.subtle, bg: theme.surfaceRaised }));
|
|
896
|
-
return result;
|
|
897
|
-
}
|
|
898
|
-
function subagentLines(message, width) {
|
|
899
|
-
const card = message.subagentCard;
|
|
900
|
-
if (!card)
|
|
901
|
-
return [];
|
|
902
|
-
const done = card.status === "completed";
|
|
903
|
-
const failed = card.status === "failed";
|
|
904
|
-
const color = failed ? theme.red : done ? theme.green : theme.cyan;
|
|
905
|
-
const output = card.errorMessage ?? card.resultSummary ?? card.task;
|
|
906
|
-
return [
|
|
907
|
-
line(` \u21B3 ${card.name} ${card.status}`, { fg: color, bold: true }),
|
|
908
|
-
...wrapText(output, Math.max(4, width - 6)).slice(0, 4).map((text) => line(` ${text}`, { fg: theme.muted })),
|
|
909
|
-
line("")
|
|
910
|
-
];
|
|
911
|
-
}
|
|
912
|
-
function isVisibleSystemMessage(message) {
|
|
913
|
-
return message.sourceAdapter === "session-comm" || message.id.startsWith("tell-");
|
|
914
|
-
}
|
|
915
|
-
function messageLines(message, width, userId) {
|
|
916
|
-
if (message.kind === "system" || message.authorId === "system" && !isVisibleSystemMessage(message)) {
|
|
917
|
-
return [];
|
|
918
|
-
}
|
|
919
|
-
if (message.kind === "subagent" && message.subagentCard)
|
|
920
|
-
return subagentLines(message, width);
|
|
921
|
-
if (message.kind === "tool") {
|
|
922
|
-
const [title = "Tool", ...details] = safeText(message.text).split(`
|
|
923
|
-
`);
|
|
924
|
-
const done = Boolean(message.editedAt);
|
|
925
|
-
return [
|
|
926
|
-
line(` ${done ? "\u2713" : "\u25CF"} ${title}`, {
|
|
927
|
-
fg: done ? theme.green : theme.cyan,
|
|
928
|
-
dim: done
|
|
929
|
-
}),
|
|
930
|
-
...details.slice(0, 2).flatMap((detail) => wrapText(detail, Math.max(4, width - 6)).slice(0, 1).map((text) => line(` ${text}`, {
|
|
931
|
-
fg: detail.startsWith("-") ? theme.red : detail.startsWith("+") ? theme.green : theme.muted,
|
|
932
|
-
dim: true
|
|
933
|
-
})))
|
|
934
|
-
];
|
|
935
|
-
}
|
|
936
|
-
const own = message.authorId === userId;
|
|
937
|
-
const ai = message.authorId === "ai";
|
|
938
|
-
const icon = own ? "\u203A" : ai ? "\u25CF" : "\u2022";
|
|
939
|
-
const body = ai ? renderMarkdown(message.text, Math.max(4, width - 6)) : wrapText(message.text, Math.max(4, width - 6)).map((text, index) => line(`${index === 0 ? ` ${icon} ` : " "}${text}`, {
|
|
940
|
-
fg: theme.text,
|
|
941
|
-
bg: theme.surfaceRaised
|
|
942
|
-
}));
|
|
943
|
-
if (ai) {
|
|
944
|
-
const firstContent = body.findIndex((item) => item.text.trim().length > 0);
|
|
945
|
-
if (firstContent >= 0) {
|
|
946
|
-
const first = body[firstContent];
|
|
947
|
-
body[firstContent] = {
|
|
948
|
-
...first,
|
|
949
|
-
text: ` ${icon} ${first.text.trimStart()}`
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
if (!ai) {
|
|
954
|
-
const padding = line("", { bg: theme.surfaceRaised });
|
|
955
|
-
return [padding, ...body, padding, line("")];
|
|
956
|
-
}
|
|
957
|
-
return [...body, line("")];
|
|
958
|
-
}
|
|
959
|
-
function activityDetail(value) {
|
|
960
|
-
const raw = value?.trim();
|
|
961
|
-
if (!raw)
|
|
962
|
-
return;
|
|
963
|
-
if (raw.startsWith("Working\u2026"))
|
|
964
|
-
return;
|
|
965
|
-
if (raw.startsWith("Thinking\u2026"))
|
|
966
|
-
return "Thinking";
|
|
967
|
-
const running = raw.match(/^(.*?) running \d+s$/u);
|
|
968
|
-
return running?.[1]?.trim() || raw;
|
|
969
|
-
}
|
|
970
|
-
function activityLines(state, animationFrame = 0, nowMs = terminalNowMs()) {
|
|
971
|
-
const topic = activeTopic(state);
|
|
972
|
-
if (!topic)
|
|
973
|
-
return [];
|
|
974
|
-
const activity = state.activity[topic.id];
|
|
975
|
-
if (!activity)
|
|
976
|
-
return [];
|
|
977
|
-
const result = [];
|
|
978
|
-
if (activity.running) {
|
|
979
|
-
const detail = activityDetail(activity.status);
|
|
980
|
-
const lastToolLabel = activity.tools.at(-1)?.label;
|
|
981
|
-
const elapsed = workingElapsedSeconds(activity.startedAtMs, nowMs);
|
|
982
|
-
result.push(line(` ${workingFrame(animationFrame)} Working \xB7 ${formatElapsedDuration(elapsed)}${detail && detail !== lastToolLabel ? ` \xB7 ${detail}` : ""}`, { fg: theme.amber, bold: true }));
|
|
983
|
-
} else if (activity.error) {
|
|
984
|
-
result.push(line(` ! ${activity.error}`, { fg: theme.red }));
|
|
985
|
-
}
|
|
986
|
-
if (result.length > 0)
|
|
987
|
-
result.push(line(""));
|
|
988
|
-
return result;
|
|
989
|
-
}
|
|
990
|
-
function taskLines(state, width) {
|
|
991
|
-
const taskPanel = activeTaskPanel(state);
|
|
992
|
-
if (!taskPanel)
|
|
993
|
-
return [];
|
|
994
|
-
return [
|
|
995
|
-
line(" \u25EB Tasks", { fg: theme.amber, bold: true }),
|
|
996
|
-
...safeText(taskPanel.text).split(`
|
|
997
|
-
`).slice(1, 6).flatMap((task) => wrapText(task.trimStart(), Math.max(4, width - 6)).map((text) => line(` ${text}`, { fg: theme.muted }))),
|
|
998
|
-
line("")
|
|
999
|
-
];
|
|
1000
|
-
}
|
|
1001
|
-
function helpLines() {
|
|
1002
|
-
return [
|
|
1003
|
-
line(" Keyboard", { fg: theme.accent, bold: true }),
|
|
1004
|
-
line(""),
|
|
1005
|
-
line(" Alt-Enter newline"),
|
|
1006
|
-
line(" \u2190 \u2192 move \xB7 Ctrl/Alt-\u2190 \u2192 move by word \xB7 \u2191 \u2193 history"),
|
|
1007
|
-
line(" Ctrl-W delete word \xB7 Ctrl-U/K clear before/after cursor"),
|
|
1008
|
-
line(" Mouse wheel / PgUp/PgDn scroll \xB7 Ctrl-E load older \xB7 Ctrl-T transcript"),
|
|
1009
|
-
line(" Ctrl-O topics \xB7 Ctrl-P/N previous/next topic"),
|
|
1010
|
-
line(" Esc/Ctrl-C stop active turn \xB7 Ctrl-C twice when idle to quit"),
|
|
1011
|
-
line(""),
|
|
1012
|
-
line(" Commands", { fg: theme.cyan, bold: true }),
|
|
1013
|
-
line(" /new /model /effort /topics /fork /spawn /del /copy"),
|
|
1014
|
-
line(" /abort /help /quit", { fg: theme.muted })
|
|
1015
|
-
];
|
|
1016
|
-
}
|
|
1017
|
-
function tokenCount(value) {
|
|
1018
|
-
if (value === undefined || !Number.isFinite(value))
|
|
1019
|
-
return "unavailable";
|
|
1020
|
-
if (value >= 1e6)
|
|
1021
|
-
return `${(value / 1e6).toFixed(2)}M`;
|
|
1022
|
-
if (value >= 1000)
|
|
1023
|
-
return `${(value / 1000).toFixed(value >= 1e5 ? 0 : 1)}K`;
|
|
1024
|
-
return String(Math.round(value));
|
|
1025
|
-
}
|
|
1026
|
-
function statusLines(state) {
|
|
1027
|
-
const topic = activeTopic(state);
|
|
1028
|
-
const latest = activeMessages(state).slice().reverse().find((message) => message.authorId === "ai" && message.usage);
|
|
1029
|
-
const usage = latest?.usage;
|
|
1030
|
-
const ratio = usage?.context !== undefined && usage.contextWindow ? Math.round(usage.context / usage.contextWindow * 100) : undefined;
|
|
1031
|
-
return [
|
|
1032
|
-
line(" Status", { fg: theme.accent, bold: true }),
|
|
1033
|
-
line(""),
|
|
1034
|
-
line(` Topic ${topic?.title ?? "none"}`),
|
|
1035
|
-
line(` Agent ${topic?.agent ?? "none"}`),
|
|
1036
|
-
line(` Model ${effectiveTopicModel(topic)}`),
|
|
1037
|
-
line(` Effort ${topic?.effectiveEffort ?? topic?.defaultEffort ?? "-"}`),
|
|
1038
|
-
line(""),
|
|
1039
|
-
line(` Context ${tokenCount(usage?.context)} / ${tokenCount(usage?.contextWindow)}${ratio === undefined ? "" : ` (${ratio}%)`}`, { fg: ratio !== undefined && ratio >= 80 ? theme.amber : theme.text }),
|
|
1040
|
-
line(" Measured on the latest model request", { fg: theme.muted, dim: true }),
|
|
1041
|
-
line(""),
|
|
1042
|
-
line(` Last turn input ${tokenCount(usage?.input)} \xB7 output ${tokenCount(usage?.output)}`),
|
|
1043
|
-
line(` Cache read ${tokenCount(usage?.cachedInput)}`),
|
|
1044
|
-
line(" Turn input is aggregate spend; it is not context size.", {
|
|
1045
|
-
fg: theme.muted,
|
|
1046
|
-
dim: true
|
|
1047
|
-
}),
|
|
1048
|
-
line(""),
|
|
1049
|
-
line(" Esc close", { fg: theme.muted })
|
|
1050
|
-
];
|
|
1051
|
-
}
|
|
1052
|
-
function plainTranscript(state) {
|
|
1053
|
-
const topic = activeTopic(state);
|
|
1054
|
-
const rows = [`# ${topic?.title ?? "Conversation"}`];
|
|
1055
|
-
for (const message of activeMessages(state).filter((item) => !item.id.startsWith("tasks-") && item.kind !== "tool" && item.kind !== "system" && (item.authorId !== "system" || isVisibleSystemMessage(item)))) {
|
|
1056
|
-
const author = message.authorId === state.userId ? "You" : message.authorId === "ai" ? state.aiName : "System";
|
|
1057
|
-
rows.push("", `${author}:`, safeText(message.text));
|
|
1058
|
-
}
|
|
1059
|
-
return rows.join(`
|
|
1060
|
-
`);
|
|
1061
|
-
}
|
|
1062
|
-
function topicOverlayLines(state, width, height, animationFrame = 0) {
|
|
1063
|
-
const indexedTopics = state.topics.map((topic, topicIndex) => ({ topic, topicIndex }));
|
|
1064
|
-
const general = indexedTopics.filter(({ topic }) => topic.title.toLowerCase() === "general");
|
|
1065
|
-
const otherTopics = indexedTopics.filter(({ topic }) => topic.title.toLowerCase() !== "general");
|
|
1066
|
-
const entries = [];
|
|
1067
|
-
if (general.length > 0) {
|
|
1068
|
-
entries.push({ kind: "manager" }, ...general.map(({ topic, topicIndex }) => ({ kind: "topic", topic, topicIndex })));
|
|
1069
|
-
}
|
|
1070
|
-
if (otherTopics.length > 0) {
|
|
1071
|
-
if (general.length > 0)
|
|
1072
|
-
entries.push({ kind: "separator" });
|
|
1073
|
-
entries.push({ kind: "heading", label: "Worker" });
|
|
1074
|
-
entries.push(...otherTopics.map(({ topic, topicIndex }) => ({
|
|
1075
|
-
kind: "topic",
|
|
1076
|
-
topic,
|
|
1077
|
-
topicIndex
|
|
1078
|
-
})));
|
|
1079
|
-
}
|
|
1080
|
-
for (const kind of ["memory", "cron"]) {
|
|
1081
|
-
const sessions = state.backgroundSessions.filter((session) => session.kind === kind);
|
|
1082
|
-
if (sessions.length === 0)
|
|
1083
|
-
continue;
|
|
1084
|
-
if (entries.length > 0)
|
|
1085
|
-
entries.push({ kind: "separator" });
|
|
1086
|
-
entries.push({ kind: "heading", label: kind === "memory" ? "Memory" : "Cron" });
|
|
1087
|
-
entries.push(...sessions.map((session) => ({
|
|
1088
|
-
kind: "background",
|
|
1089
|
-
sessionId: session.id,
|
|
1090
|
-
label: session.title,
|
|
1091
|
-
status: session.status,
|
|
1092
|
-
active: session.active ?? true
|
|
1093
|
-
})));
|
|
1094
|
-
}
|
|
1095
|
-
const visibleCount = Math.max(1, height - 3);
|
|
1096
|
-
const selectedEntryIndex = entries.findIndex((entry) => entry.kind === "topic" && !state.topicPickerBackgroundId && entry.topicIndex === state.topicPickerIndex || entry.kind === "background" && entry.sessionId === state.topicPickerBackgroundId);
|
|
1097
|
-
let start = Math.min(Math.max(0, entries.length - visibleCount), Math.max(0, selectedEntryIndex - visibleCount + 1));
|
|
1098
|
-
if (start > 0 && entries[start - 1]?.kind === "manager" && selectedEntryIndex - (start - 1) < visibleCount) {
|
|
1099
|
-
start -= 1;
|
|
1100
|
-
}
|
|
1101
|
-
return [
|
|
1102
|
-
line(" Topics", { fg: theme.accent, bold: true }),
|
|
1103
|
-
line(state.topicPickerRoot ? " \u2191\u2193 select \xB7 Enter open \xB7 N new \xB7 D/Del delete \xB7 Esc/Ctrl-C exit" : " \u2191\u2193 select \xB7 Enter open \xB7 N new \xB7 D/Del delete \xB7 Esc close \xB7 Ctrl-C exit; work continues", { fg: theme.muted }),
|
|
1104
|
-
line(""),
|
|
1105
|
-
...entries.length === 0 ? [line(" No topics yet \xB7 Press N to create one", { fg: theme.muted })] : entries.slice(start, start + visibleCount).map((entry) => {
|
|
1106
|
-
if (entry.kind === "manager") {
|
|
1107
|
-
return line(" Manager", { fg: theme.cyan, bold: true });
|
|
1108
|
-
}
|
|
1109
|
-
if (entry.kind === "heading") {
|
|
1110
|
-
return line(` ${entry.label}`, { fg: theme.cyan, bold: true });
|
|
1111
|
-
}
|
|
1112
|
-
if (entry.kind === "separator") {
|
|
1113
|
-
return line(` ${"\u2500".repeat(Math.max(1, width - 4))}`, { fg: theme.border });
|
|
1114
|
-
}
|
|
1115
|
-
if (entry.kind === "background") {
|
|
1116
|
-
const selected2 = entry.sessionId === state.topicPickerBackgroundId;
|
|
1117
|
-
return line(` ${selected2 ? "\u203A" : " "} ${entry.active ? workingFrame(animationFrame) : "\u25CB"} ${entry.label} \xB7 ${entry.status}`, {
|
|
1118
|
-
fg: selected2 ? theme.text : entry.active ? theme.green : theme.muted,
|
|
1119
|
-
bg: selected2 ? theme.selected : theme.canvas,
|
|
1120
|
-
bold: selected2
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
const { topic, topicIndex } = entry;
|
|
1124
|
-
const selected = topicIndex === state.topicPickerIndex;
|
|
1125
|
-
const running = state.activity[topic.id]?.running;
|
|
1126
|
-
const childPrefix = topic.isSubagent ? " \u21B3 " : "";
|
|
1127
|
-
return line(` ${selected ? "\u203A" : " "} ${childPrefix}${running ? workingFrame(animationFrame) : "\u25CB"} ${topic.title} \xB7 ${topic.agent ?? "no agent"} \xB7 ${effectiveTopicModel(topic)} \xB7 ${effectiveTopicEffort(topic)}`, {
|
|
1128
|
-
fg: selected ? theme.text : running ? theme.green : theme.muted,
|
|
1129
|
-
bg: selected ? theme.selected : theme.canvas,
|
|
1130
|
-
bold: selected
|
|
1131
|
-
});
|
|
1132
|
-
})
|
|
1133
|
-
];
|
|
1134
|
-
}
|
|
1135
|
-
function backgroundSessionLines(state, width, nowMs = terminalNowMs()) {
|
|
1136
|
-
const session = pickedBackgroundSession(state);
|
|
1137
|
-
if (!session)
|
|
1138
|
-
return [line(" This background session has finished.", { fg: theme.muted })];
|
|
1139
|
-
const elapsed = formatElapsedDuration(Math.max(0, Math.floor((nowMs - Date.parse(session.startedAt)) / 1000)));
|
|
1140
|
-
return [
|
|
1141
|
-
line(` ${session.kind === "memory" ? "Memory" : "Cron"} \xB7 read-only`, {
|
|
1142
|
-
fg: theme.accent,
|
|
1143
|
-
bold: true
|
|
1144
|
-
}),
|
|
1145
|
-
line(session.kind === "cron" ? " Esc back \xB7 session stays available between runs" : " Esc back \xB7 this entry disappears when archiving finishes", { fg: theme.muted }),
|
|
1146
|
-
line(""),
|
|
1147
|
-
line(` ${session.title}`, { bold: true }),
|
|
1148
|
-
line(` ${session.status}${session.active === false ? "" : ` \xB7 ${elapsed}`}`, {
|
|
1149
|
-
fg: session.active === false ? theme.muted : theme.green
|
|
1150
|
-
}),
|
|
1151
|
-
...session.agent || session.model || session.effort ? [
|
|
1152
|
-
line(` ${session.agent ?? "-"} \xB7 ${session.model ?? "default"} \xB7 ${session.effort ?? "default"}`, { fg: theme.muted })
|
|
1153
|
-
] : [],
|
|
1154
|
-
...session.prompt ? [
|
|
1155
|
-
line(""),
|
|
1156
|
-
line(` ${session.promptTitle ?? "Prompt"}`, { fg: theme.cyan, bold: true }),
|
|
1157
|
-
...session.prompt.split(`
|
|
1158
|
-
`).flatMap((part) => wrapText(part || " ", Math.max(4, width - 4))).map((part) => line(` ${safeText(part)}`, { fg: theme.text }))
|
|
1159
|
-
] : [],
|
|
1160
|
-
line(""),
|
|
1161
|
-
line(" Activity", { fg: theme.cyan, bold: true }),
|
|
1162
|
-
...session.steps.length > 0 ? session.steps.map((step) => line(` \u25CB ${safeText(step)}`, { fg: theme.muted })) : [line(" \u25CB Waiting for runtime activity", { fg: theme.muted })]
|
|
1163
|
-
];
|
|
1164
|
-
}
|
|
1165
|
-
function modelOverlayLines(state, height) {
|
|
1166
|
-
const currentModel = effectiveTopicModel(activeTopic(state));
|
|
1167
|
-
const modelColumnWidth = Math.max(...SELECTABLE_MODELS.map(({ model }) => `${model} (current)`.length));
|
|
1168
|
-
const visibleCount = Math.max(1, height - 3);
|
|
1169
|
-
const start = Math.min(Math.max(0, SELECTABLE_MODELS.length - visibleCount), Math.max(0, state.modelPickerIndex - visibleCount + 1));
|
|
1170
|
-
return [
|
|
1171
|
-
line(" Models", { fg: theme.accent, bold: true }),
|
|
1172
|
-
line(" \u2191\u2193 select \xB7 Enter apply \xB7 Esc close", { fg: theme.muted }),
|
|
1173
|
-
line(""),
|
|
1174
|
-
...SELECTABLE_MODELS.slice(start, start + visibleCount).map(({ model, description }, visibleIndex) => {
|
|
1175
|
-
const index = start + visibleIndex;
|
|
1176
|
-
const selected = index === state.modelPickerIndex;
|
|
1177
|
-
const current = model === currentModel;
|
|
1178
|
-
const label = `${model}${current ? " (current)" : ""}`.padEnd(modelColumnWidth);
|
|
1179
|
-
return line(` ${selected ? "\u203A" : " "} ${label} ${description}`, {
|
|
1180
|
-
fg: selected ? theme.text : current ? theme.green : theme.muted,
|
|
1181
|
-
bg: selected ? theme.selected : theme.canvas,
|
|
1182
|
-
bold: selected
|
|
1183
|
-
});
|
|
1184
|
-
})
|
|
1185
|
-
];
|
|
1186
|
-
}
|
|
1187
|
-
function vaultOverlayLines(state, width, height) {
|
|
1188
|
-
const header = [
|
|
1189
|
-
line(" Vault", { fg: theme.accent, bold: true }),
|
|
1190
|
-
line(" Encrypted locally \xB7 secret values are never displayed", { fg: theme.muted }),
|
|
1191
|
-
line("")
|
|
1192
|
-
];
|
|
1193
|
-
if (state.vaultMode === "confirm-delete") {
|
|
1194
|
-
const selected = state.vaultEntries[state.vaultPickerIndex];
|
|
1195
|
-
return [
|
|
1196
|
-
...header,
|
|
1197
|
-
line(` Delete ${selected?.key ?? "this key"}?`, { fg: theme.red, bold: true }),
|
|
1198
|
-
line(""),
|
|
1199
|
-
line(" This cannot be undone. The secret value will be removed.", { fg: theme.muted }),
|
|
1200
|
-
line(" Press y to delete or n to cancel.", { fg: theme.amber })
|
|
1201
|
-
].slice(0, height);
|
|
1202
|
-
}
|
|
1203
|
-
if (state.vaultMode !== "list") {
|
|
1204
|
-
const step = state.vaultMode === "key" ? 1 : state.vaultMode === "value" ? 2 : 3;
|
|
1205
|
-
const action = state.vaultEditing ? "Update secret" : "Add secret";
|
|
1206
|
-
const guidance = state.vaultMode === "key" ? "Choose a recognizable key, for example GITHUB_TOKEN." : state.vaultMode === "value" ? "Enter the secret value. Input is hidden and is never saved to history." : "Add an optional description so the agent knows when to use this key.";
|
|
1207
|
-
return [
|
|
1208
|
-
...header,
|
|
1209
|
-
line(` ${action} \xB7 ${step}/3`, { bold: true }),
|
|
1210
|
-
...state.vaultDraftKey ? [line(` Key ${state.vaultDraftKey}`, { fg: theme.muted })] : [],
|
|
1211
|
-
line(""),
|
|
1212
|
-
...wrapText(guidance, Math.max(4, width - 4)).map((text) => line(` ${text}`)),
|
|
1213
|
-
...state.vaultNotice ? [line(""), line(` ! ${state.vaultNotice}`, { fg: theme.amber })] : []
|
|
1214
|
-
].slice(0, height);
|
|
1215
|
-
}
|
|
1216
|
-
const entries = state.vaultEntries;
|
|
1217
|
-
const visibleCount = Math.max(1, height - 7);
|
|
1218
|
-
const start = Math.min(Math.max(0, entries.length - visibleCount), Math.max(0, state.vaultPickerIndex - visibleCount + 1));
|
|
1219
|
-
return [
|
|
1220
|
-
...header,
|
|
1221
|
-
...entries.length === 0 ? [
|
|
1222
|
-
line(" No secrets stored", { bold: true }),
|
|
1223
|
-
line(" Press N to add your first key.", { fg: theme.muted })
|
|
1224
|
-
] : entries.slice(start, start + visibleCount).map((entry, visibleIndex) => {
|
|
1225
|
-
const selected = start + visibleIndex === state.vaultPickerIndex;
|
|
1226
|
-
const description = entry.description || "No description";
|
|
1227
|
-
return line(` ${selected ? "\u203A" : " "} ${entry.key} ${description}`, {
|
|
1228
|
-
fg: selected ? theme.text : theme.muted,
|
|
1229
|
-
bg: selected ? theme.selected : theme.canvas,
|
|
1230
|
-
bold: selected
|
|
1231
|
-
});
|
|
1232
|
-
}),
|
|
1233
|
-
...state.vaultNotice ? [line(""), line(` ${state.vaultNotice}`, { fg: theme.green })] : []
|
|
1234
|
-
].slice(0, height);
|
|
1235
|
-
}
|
|
1236
|
-
function effortOverlayLines(state) {
|
|
1237
|
-
const topic = activeTopic(state);
|
|
1238
|
-
const currentEffort = topic?.effectiveEffort ?? topic?.defaultEffort;
|
|
1239
|
-
const efforts = topic?.agent ? getRegistry(topic.agent).validEfforts : EFFORT_VALUES;
|
|
1240
|
-
return [
|
|
1241
|
-
line(" Reasoning effort", { fg: theme.accent, bold: true }),
|
|
1242
|
-
line(" \u2191\u2193 select \xB7 Enter apply \xB7 Esc close", { fg: theme.muted }),
|
|
1243
|
-
line(""),
|
|
1244
|
-
...efforts.map((effort, index) => {
|
|
1245
|
-
const selected = index === state.effortPickerIndex;
|
|
1246
|
-
const current = effort === currentEffort;
|
|
1247
|
-
return line(` ${selected ? "\u203A" : " "} ${effort}${current ? " (current)" : ""}`, {
|
|
1248
|
-
fg: selected ? theme.text : current ? theme.green : theme.muted,
|
|
1249
|
-
bg: selected ? theme.selected : theme.canvas,
|
|
1250
|
-
bold: selected
|
|
1251
|
-
});
|
|
1252
|
-
})
|
|
1253
|
-
];
|
|
1254
|
-
}
|
|
1255
|
-
function conversationContentLines(state, width, animationFrame = 0, nowMs = terminalNowMs()) {
|
|
1256
|
-
const all = [];
|
|
1257
|
-
for (const message of activeMessages(state).filter((item) => !item.id.startsWith("tasks-") && item.kind !== "system" && (item.authorId !== "system" || isVisibleSystemMessage(item)))) {
|
|
1258
|
-
all.push(...messageLines(message, width, state.userId));
|
|
1259
|
-
}
|
|
1260
|
-
all.push(...activityLines(state, animationFrame, nowMs), ...taskLines(state, width));
|
|
1261
|
-
if (all.length === 0) {
|
|
1262
|
-
all.push(line(""), line(" \u2726 Start a conversation", { fg: theme.accent, bold: true }), line(" Ask, build, research, or delegate from the composer below.", { fg: theme.muted }));
|
|
1263
|
-
}
|
|
1264
|
-
return all;
|
|
1265
|
-
}
|
|
1266
|
-
function conversationLines(state, width, height, animationFrame = 0, nowMs = terminalNowMs()) {
|
|
1267
|
-
if (state.overlay === "help")
|
|
1268
|
-
return helpLines().slice(0, height);
|
|
1269
|
-
if (state.overlay === "status")
|
|
1270
|
-
return statusLines(state).slice(0, height);
|
|
1271
|
-
if (state.overlay === "topics")
|
|
1272
|
-
return topicOverlayLines(state, width, height, animationFrame).slice(0, height);
|
|
1273
|
-
if (state.overlay === "background-session")
|
|
1274
|
-
return backgroundSessionLines(state, width, nowMs).slice(0, height);
|
|
1275
|
-
if (state.overlay === "models")
|
|
1276
|
-
return modelOverlayLines(state, height).slice(0, height);
|
|
1277
|
-
if (state.overlay === "effort")
|
|
1278
|
-
return effortOverlayLines(state).slice(0, height);
|
|
1279
|
-
if (state.overlay === "vault")
|
|
1280
|
-
return vaultOverlayLines(state, width, height);
|
|
1281
|
-
if (state.creatingTopic) {
|
|
1282
|
-
return [
|
|
1283
|
-
line(""),
|
|
1284
|
-
line(" New topic", { fg: theme.accent, bold: true }),
|
|
1285
|
-
line(""),
|
|
1286
|
-
line(" Type only the topic name in the composer below."),
|
|
1287
|
-
line(" Enter create \xB7 Esc cancel", { fg: theme.muted })
|
|
1288
|
-
].slice(0, height);
|
|
1289
|
-
}
|
|
1290
|
-
if (state.overlay === "transcript") {
|
|
1291
|
-
return [
|
|
1292
|
-
line(" Transcript \xB7 Ctrl-T close", { fg: theme.accent, bold: true }),
|
|
1293
|
-
line(""),
|
|
1294
|
-
...plainTranscript(state).split(`
|
|
1295
|
-
`).flatMap((text) => wrapText(text, Math.max(4, width - 4)).map((part) => line(` ${part}`)))
|
|
1296
|
-
].slice(0, height);
|
|
1297
|
-
}
|
|
1298
|
-
if (state.overlay === "confirm-delete") {
|
|
1299
|
-
const topic = state.topics.find((candidate) => candidate.id === state.pendingDeleteTopicId);
|
|
1300
|
-
return [
|
|
1301
|
-
line(""),
|
|
1302
|
-
line(` Delete \u201C${topic?.title ?? "this topic"}\u201D?`, { fg: theme.red, bold: true }),
|
|
1303
|
-
line(""),
|
|
1304
|
-
line(" The transcript is archived before the topic and its runtime state are removed."),
|
|
1305
|
-
line(" Press y to delete or n to cancel.", { fg: theme.amber })
|
|
1306
|
-
].slice(0, height);
|
|
1307
|
-
}
|
|
1308
|
-
const all = conversationContentLines(state, width, animationFrame, nowMs);
|
|
1309
|
-
const { contentHeight, maxOffset, offset } = conversationViewport(all.length, height, state.scrollOffset);
|
|
1310
|
-
const end = all.length - offset;
|
|
1311
|
-
const visible = all.slice(Math.max(0, end - contentHeight), end);
|
|
1312
|
-
const history = state.activeTopicId ? state.messageHistory[state.activeTopicId] : undefined;
|
|
1313
|
-
const marker = offset >= maxOffset && history?.loading ? " \u2191 Loading older messages\u2026" : offset >= maxOffset && history?.hasMore ? " \u2191 Loaded history start \xB7 Ctrl-E load older" : offset >= maxOffset ? " \u2191 Start of conversation" : ` \u2191 history \xB7 ${offset} lines from latest \xB7 wheel down/PgDn to return`;
|
|
1314
|
-
return offset > 0 ? [
|
|
1315
|
-
line(marker, {
|
|
1316
|
-
fg: theme.amber,
|
|
1317
|
-
dim: true
|
|
1318
|
-
}),
|
|
1319
|
-
...visible
|
|
1320
|
-
] : visible;
|
|
1321
|
-
}
|
|
1322
|
-
function conversationViewport(lineCount, height, requestedOffset) {
|
|
1323
|
-
if (lineCount <= height)
|
|
1324
|
-
return { contentHeight: height, maxOffset: 0, offset: 0 };
|
|
1325
|
-
const contentHeight = Math.max(1, height - 1);
|
|
1326
|
-
const maxOffset = Math.max(0, lineCount - contentHeight);
|
|
1327
|
-
return {
|
|
1328
|
-
contentHeight,
|
|
1329
|
-
maxOffset,
|
|
1330
|
-
offset: Math.min(maxOffset, Math.max(0, requestedOffset))
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
function decisionPane(state, width) {
|
|
1334
|
-
if (state.overlay)
|
|
1335
|
-
return [];
|
|
1336
|
-
const ask = activeQuestion(state);
|
|
1337
|
-
const question = ask?.askUserQuestion;
|
|
1338
|
-
if (!ask || !question?.choices.length)
|
|
1339
|
-
return [];
|
|
1340
|
-
const selected = Math.min(state.askChoiceIndex, question.choices.length - 1);
|
|
1341
|
-
const content = [
|
|
1342
|
-
line(` ${question.question}`, { bold: true }),
|
|
1343
|
-
...question.choices.map((choice, index) => line(` ${index === selected ? "\u25CF" : "\u25CB"} ${choice.label}${choice.description ? ` \u2014 ${choice.description}` : ""}`, {
|
|
1344
|
-
fg: index === selected ? theme.text : theme.muted,
|
|
1345
|
-
bg: index === selected ? theme.selected : theme.surface,
|
|
1346
|
-
bold: index === selected
|
|
1347
|
-
}))
|
|
1348
|
-
];
|
|
1349
|
-
return framePane("decision required \xB7 \u2191\u2193 select \xB7 Enter confirm", content, width, Math.min(8, Math.max(4, content.length + 2)), { active: true, accent: theme.amber });
|
|
1350
|
-
}
|
|
1351
|
-
function wrappedCursorPosition(value, codePointColumn, width) {
|
|
1352
|
-
let line2 = 0;
|
|
1353
|
-
let column = 0;
|
|
1354
|
-
for (const character of Array.from(value).slice(0, Math.max(0, codePointColumn))) {
|
|
1355
|
-
const characterWidth = runeWidth(character);
|
|
1356
|
-
if (column + characterWidth > width && column > 0) {
|
|
1357
|
-
line2 += 1;
|
|
1358
|
-
column = 0;
|
|
1359
|
-
}
|
|
1360
|
-
column += characterWidth;
|
|
1361
|
-
}
|
|
1362
|
-
return { line: line2, column };
|
|
1363
|
-
}
|
|
1364
|
-
function inputVisualLines(state, width) {
|
|
1365
|
-
const secretInput = state.overlay === "vault" && state.vaultMode === "value";
|
|
1366
|
-
const displayInput = secretInput ? state.input.split(`
|
|
1367
|
-
`).map((row) => "*".repeat(Array.from(row).length)).join(`
|
|
1368
|
-
`) : state.input;
|
|
1369
|
-
if (!displayInput) {
|
|
1370
|
-
const placeholder = state.overlay === "vault" ? state.vaultMode === "key" ? "Type a key name\u2026" : state.vaultMode === "value" ? "Type the secret value\u2026" : "Optional description\u2026" : state.creatingTopic ? "Type a topic name\u2026" : "Type a message or /command\u2026";
|
|
1371
|
-
return {
|
|
1372
|
-
lines: [line(` \u203A ${placeholder}`, { fg: theme.subtle, bg: theme.surfaceRaised })],
|
|
1373
|
-
cursorLine: 0,
|
|
1374
|
-
cursorColumn: 5
|
|
1375
|
-
};
|
|
1376
|
-
}
|
|
1377
|
-
const contentWidth = Math.max(4, width - 5);
|
|
1378
|
-
const result = [];
|
|
1379
|
-
let cursorLine = 0;
|
|
1380
|
-
let cursorColumn = 5;
|
|
1381
|
-
for (const [row, inputLine] of displayInput.split(`
|
|
1382
|
-
`).entries()) {
|
|
1383
|
-
const firstVisualLine = result.length;
|
|
1384
|
-
if (row === state.inputCursor.row) {
|
|
1385
|
-
const cursor = wrappedCursorPosition(inputLine, state.inputCursor.col, contentWidth);
|
|
1386
|
-
cursorLine = firstVisualLine + cursor.line;
|
|
1387
|
-
cursorColumn = 5 + cursor.column;
|
|
1388
|
-
}
|
|
1389
|
-
for (const [visualIndex, wrapped] of wrapText(inputLine, contentWidth).entries()) {
|
|
1390
|
-
result.push(line(`${row === 0 && visualIndex === 0 ? " \u203A " : " "}${wrapped}`, {
|
|
1391
|
-
bg: theme.surfaceRaised
|
|
1392
|
-
}));
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
return { lines: result, cursorLine, cursorColumn };
|
|
1396
|
-
}
|
|
1397
|
-
function composerPane(state, width) {
|
|
1398
|
-
const title = state.creatingTopic ? "new topic \xB7 type a name \xB7 Enter create" : state.overlay === "vault" ? `${state.vaultMode === "key" ? "key" : state.vaultMode === "value" ? "secret value" : "description"} \xB7 Enter continue \xB7 Esc cancel` : "Ctrl-O topics";
|
|
1399
|
-
const visual = inputVisualLines(state, width);
|
|
1400
|
-
let inputStart = Math.max(0, visual.lines.length - 5);
|
|
1401
|
-
if (visual.cursorLine < inputStart)
|
|
1402
|
-
inputStart = visual.cursorLine;
|
|
1403
|
-
else if (visual.cursorLine >= inputStart + 5)
|
|
1404
|
-
inputStart = visual.cursorLine - 4;
|
|
1405
|
-
const inputLines = visual.lines.slice(inputStart, inputStart + 5);
|
|
1406
|
-
const visibleCursorLine = visual.cursorLine - inputStart;
|
|
1407
|
-
const suggestionsDisabled = state.creatingTopic || state.overlay === "vault";
|
|
1408
|
-
const commands = suggestionsDisabled ? [] : commandSuggestions(state.input);
|
|
1409
|
-
const cap = Math.max(0, 6 - inputLines.length);
|
|
1410
|
-
let suggestionLines;
|
|
1411
|
-
if (commands.length > 0) {
|
|
1412
|
-
suggestionLines = commands.slice(0, cap).map((command, index) => line(` ${index === state.suggestionIndex ? "\u203A" : " "} ${command.usage} ${command.description}`, {
|
|
1413
|
-
fg: index === state.suggestionIndex ? theme.text : theme.muted,
|
|
1414
|
-
bg: index === state.suggestionIndex ? theme.selected : theme.surface
|
|
1415
|
-
}));
|
|
1416
|
-
} else if (!suggestionsDisabled) {
|
|
1417
|
-
const cursor = state.inputCursor;
|
|
1418
|
-
const lineText = state.input.split(`
|
|
1419
|
-
`)[cursor.row] ?? "";
|
|
1420
|
-
const paths = pathSuggestions(lineText, cursor.col);
|
|
1421
|
-
const items = paths?.items ?? [];
|
|
1422
|
-
suggestionLines = items.slice(0, cap).map((item, index) => line(` ${index === state.suggestionIndex ? "\u203A" : " "} ${item.value.slice(1)}`, {
|
|
1423
|
-
fg: index === state.suggestionIndex ? theme.text : theme.muted,
|
|
1424
|
-
bg: index === state.suggestionIndex ? theme.selected : theme.surface
|
|
1425
|
-
}));
|
|
1426
|
-
const hidden = (paths?.truncated ?? 0) + Math.max(0, items.length - suggestionLines.length);
|
|
1427
|
-
if (hidden > 0 && suggestionLines.length < cap) {
|
|
1428
|
-
suggestionLines.push(line(` \u2026 +${hidden} more`, { fg: theme.muted, dim: true }));
|
|
1429
|
-
}
|
|
1430
|
-
} else {
|
|
1431
|
-
suggestionLines = [];
|
|
1432
|
-
}
|
|
1433
|
-
const content = [line(""), ...inputLines, line(""), ...suggestionLines].slice(0, 8);
|
|
1434
|
-
const input = content.map((item) => paint(fit(item.text, width), {
|
|
1435
|
-
fg: item.fg ?? theme.text,
|
|
1436
|
-
bg: item.bg ?? theme.surfaceRaised,
|
|
1437
|
-
bold: item.bold,
|
|
1438
|
-
dim: item.dim
|
|
1439
|
-
}));
|
|
1440
|
-
const hint = paint(fit(` ${title}`, width), { fg: theme.muted, bg: theme.canvas });
|
|
1441
|
-
return {
|
|
1442
|
-
lines: state.creatingTopic ? [hint, ...input] : [...input, hint],
|
|
1443
|
-
cursor: {
|
|
1444
|
-
x: visual.cursorColumn,
|
|
1445
|
-
y: 2 + visibleCursorLine + (state.creatingTopic ? 1 : 0)
|
|
1446
|
-
}
|
|
1447
|
-
};
|
|
1448
|
-
}
|
|
1449
|
-
function footerLines(state, width) {
|
|
1450
|
-
if (state.creatingTopic) {
|
|
1451
|
-
return [
|
|
1452
|
-
paint(joinSides(" New topic", "\u25CB naming ", width), {
|
|
1453
|
-
fg: theme.accent,
|
|
1454
|
-
bg: theme.surfaceRaised,
|
|
1455
|
-
bold: true
|
|
1456
|
-
}),
|
|
1457
|
-
paint(joinSides("", "Esc cancel ", width), {
|
|
1458
|
-
fg: theme.muted,
|
|
1459
|
-
bg: theme.canvas
|
|
1460
|
-
})
|
|
1461
|
-
];
|
|
1462
|
-
}
|
|
1463
|
-
const topic = activeTopic(state);
|
|
1464
|
-
const running = Boolean(topic && state.activity[topic.id]?.running);
|
|
1465
|
-
return [
|
|
1466
|
-
paint(joinSides(` ${topic?.agent ?? "-"} \xB7 ${effectiveTopicModel(topic)} \xB7 ${effectiveTopicEffort(topic)}`, "", width), {
|
|
1467
|
-
fg: theme.accent,
|
|
1468
|
-
bg: theme.surfaceRaised,
|
|
1469
|
-
bold: true
|
|
1470
|
-
}),
|
|
1471
|
-
paint(joinSides("", state.notice ? `! ${state.notice} ` : state.overlay === "topics" ? state.topicPickerRoot ? "Esc/Ctrl-C exit " : "Esc close \xB7 Ctrl-C exit; work continues " : state.overlay === "background-session" ? "Esc back \xB7 read-only " : state.overlay === "vault" ? state.vaultMode === "list" ? state.vaultEntries.length > 0 ? "\u2191\u2193 select \xB7 Enter edit \xB7 N add \xB7 D delete \xB7 Esc close " : "N add \xB7 Esc close " : state.vaultMode === "confirm-delete" ? "Y delete \xB7 N cancel " : "Enter continue \xB7 Esc cancel " : running ? "Esc/Ctrl-C stop " : "Ctrl-C twice to quit ", width), {
|
|
1472
|
-
fg: state.notice ? theme.amber : theme.muted,
|
|
1473
|
-
bg: theme.canvas
|
|
1474
|
-
})
|
|
1475
|
-
];
|
|
1476
|
-
}
|
|
1477
|
-
function renderBody(lines, width, height) {
|
|
1478
|
-
return Array.from({ length: height }, (_, index) => {
|
|
1479
|
-
const item = lines[index] ?? line("");
|
|
1480
|
-
return paint(fit(item.text, width), {
|
|
1481
|
-
fg: item.fg ?? theme.text,
|
|
1482
|
-
bg: item.bg ?? theme.canvas,
|
|
1483
|
-
bold: item.bold,
|
|
1484
|
-
dim: item.dim
|
|
1485
|
-
});
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
function renderApp(state, columns, rows, animationFrame = 0, nowMs = terminalNowMs()) {
|
|
1489
|
-
return renderAppFrame(state, columns, rows, animationFrame, nowMs).frame;
|
|
1490
|
-
}
|
|
1491
|
-
function renderAppFrame(state, columns, rows, animationFrame = 0, nowMs = terminalNowMs()) {
|
|
1492
|
-
const width = Math.max(32, columns);
|
|
1493
|
-
const height = Math.max(14, rows);
|
|
1494
|
-
const footer = footerLines(state, width);
|
|
1495
|
-
const decision = decisionPane(state, width);
|
|
1496
|
-
const hideComposer = state.overlay === "background-session" || state.overlay === "vault" && (state.vaultMode === "list" || state.vaultMode === "confirm-delete");
|
|
1497
|
-
const composer = hideComposer ? { lines: [], cursor: null } : composerPane(state, width);
|
|
1498
|
-
const bodyHeight = Math.max(3, height - footer.length - decision.length - composer.lines.length);
|
|
1499
|
-
const body = renderBody(conversationLines(state, width, bodyHeight, animationFrame, nowMs), width, bodyHeight);
|
|
1500
|
-
const cursorY = composer.cursor ? body.length + decision.length + composer.cursor.y : Number.POSITIVE_INFINITY;
|
|
1501
|
-
return {
|
|
1502
|
-
frame: [...body, ...decision, ...composer.lines, ...footer].slice(0, height).join(`
|
|
1503
|
-
`),
|
|
1504
|
-
cursor: composer.cursor && cursorY <= height ? { x: composer.cursor.x, y: cursorY } : null
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
function maxConversationScrollOffset(state, columns, rows) {
|
|
1508
|
-
if (state.overlay)
|
|
1509
|
-
return 0;
|
|
1510
|
-
const width = Math.max(32, columns);
|
|
1511
|
-
const height = Math.max(14, rows);
|
|
1512
|
-
const bodyHeight = Math.max(3, height - footerLines(state, width).length - decisionPane(state, width).length - composerPane(state, width).lines.length);
|
|
1513
|
-
const lineCount = conversationContentLines(state, width).length;
|
|
1514
|
-
return conversationViewport(lineCount, bodyHeight, state.scrollOffset).maxOffset;
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
// ../../adapters/terminal/src/screen-renderer.ts
|
|
1518
|
-
var ESC2 = "\x1B[";
|
|
1519
|
-
function placeTerminalCursor(cursor) {
|
|
1520
|
-
const x = Math.max(1, Math.trunc(cursor.x));
|
|
1521
|
-
const y = Math.max(1, Math.trunc(cursor.y));
|
|
1522
|
-
return `${ESC2}${y};${x}H${ESC2}?25h`;
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
class TerminalScreenRenderer {
|
|
1526
|
-
#previousLines = [];
|
|
1527
|
-
#invalidated = true;
|
|
1528
|
-
invalidate() {
|
|
1529
|
-
this.#invalidated = true;
|
|
1530
|
-
}
|
|
1531
|
-
reset() {
|
|
1532
|
-
this.#previousLines = [];
|
|
1533
|
-
this.#invalidated = true;
|
|
1534
|
-
}
|
|
1535
|
-
update(frame) {
|
|
1536
|
-
const lines = frame.split(`
|
|
1537
|
-
`);
|
|
1538
|
-
const previous = this.#previousLines;
|
|
1539
|
-
const redrawAll = this.#invalidated;
|
|
1540
|
-
const rowCount = Math.max(lines.length, previous.length);
|
|
1541
|
-
let output = "";
|
|
1542
|
-
for (let index = 0;index < rowCount; index += 1) {
|
|
1543
|
-
const current = lines[index];
|
|
1544
|
-
if (!redrawAll && current === previous[index])
|
|
1545
|
-
continue;
|
|
1546
|
-
output += `${ESC2}${index + 1};1H${ESC2}2K${current ?? ""}`;
|
|
1547
|
-
}
|
|
1548
|
-
if (output)
|
|
1549
|
-
output += `${ESC2}H`;
|
|
1550
|
-
this.#previousLines = lines;
|
|
1551
|
-
this.#invalidated = false;
|
|
1552
|
-
return output;
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
// ../../adapters/terminal/src/selection.ts
|
|
1557
|
-
function ordered(selection) {
|
|
1558
|
-
const { anchor, focus } = selection;
|
|
1559
|
-
return anchor.y < focus.y || anchor.y === focus.y && anchor.x <= focus.x ? [anchor, focus] : [focus, anchor];
|
|
1560
|
-
}
|
|
1561
|
-
function columnSlice(value, from, to) {
|
|
1562
|
-
let output = "";
|
|
1563
|
-
let column = 0;
|
|
1564
|
-
for (const character of stripAnsi(value)) {
|
|
1565
|
-
const width = displayWidth(character);
|
|
1566
|
-
const next = column + width;
|
|
1567
|
-
if (next > from && column < to)
|
|
1568
|
-
output += character;
|
|
1569
|
-
column = next;
|
|
1570
|
-
if (column >= to)
|
|
1571
|
-
break;
|
|
1572
|
-
}
|
|
1573
|
-
return output;
|
|
1574
|
-
}
|
|
1575
|
-
function screenSelectionText(lines, selection) {
|
|
1576
|
-
const [start, end] = ordered(selection);
|
|
1577
|
-
const selected = [];
|
|
1578
|
-
for (let row = start.y;row <= end.y; row += 1) {
|
|
1579
|
-
const value = lines[row - 1] ?? "";
|
|
1580
|
-
const from = row === start.y ? Math.max(0, start.x - 1) : 0;
|
|
1581
|
-
const to = row === end.y ? Math.max(from, end.x) : Number.POSITIVE_INFINITY;
|
|
1582
|
-
selected.push(columnSlice(value, from, to).trimEnd());
|
|
1583
|
-
}
|
|
1584
|
-
return selected.join(`
|
|
1585
|
-
`).replace(/\n+$/, "");
|
|
1586
|
-
}
|
|
1587
|
-
var ANSI_PATTERN = new RegExp("\\u001b\\[[0-?]*[ -/]*[@-~]", "y");
|
|
1588
|
-
function highlightRow(value, from, to) {
|
|
1589
|
-
let output = "";
|
|
1590
|
-
let column = 0;
|
|
1591
|
-
let inverse = false;
|
|
1592
|
-
let index = 0;
|
|
1593
|
-
while (index < value.length) {
|
|
1594
|
-
ANSI_PATTERN.lastIndex = index;
|
|
1595
|
-
const ansi = ANSI_PATTERN.exec(value);
|
|
1596
|
-
if (ansi) {
|
|
1597
|
-
output += ansi[0];
|
|
1598
|
-
index += ansi[0].length;
|
|
1599
|
-
continue;
|
|
1600
|
-
}
|
|
1601
|
-
const codePoint = value.codePointAt(index);
|
|
1602
|
-
if (codePoint === undefined)
|
|
1603
|
-
break;
|
|
1604
|
-
const character = String.fromCodePoint(codePoint);
|
|
1605
|
-
const width = displayWidth(character);
|
|
1606
|
-
const selected = column + width > from && column < to;
|
|
1607
|
-
if (selected && !inverse) {
|
|
1608
|
-
output += "\x1B[7m";
|
|
1609
|
-
inverse = true;
|
|
1610
|
-
} else if (!selected && inverse) {
|
|
1611
|
-
output += "\x1B[27m";
|
|
1612
|
-
inverse = false;
|
|
1613
|
-
}
|
|
1614
|
-
output += character;
|
|
1615
|
-
column += width;
|
|
1616
|
-
index += character.length;
|
|
1617
|
-
}
|
|
1618
|
-
return inverse ? `${output}\x1B[27m` : output;
|
|
1619
|
-
}
|
|
1620
|
-
function highlightScreenSelection(frame, selection) {
|
|
1621
|
-
const [start, end] = ordered(selection);
|
|
1622
|
-
return frame.split(`
|
|
1623
|
-
`).map((line2, index) => {
|
|
1624
|
-
const row = index + 1;
|
|
1625
|
-
if (row < start.y || row > end.y)
|
|
1626
|
-
return line2;
|
|
1627
|
-
const from = row === start.y ? Math.max(0, start.x - 1) : 0;
|
|
1628
|
-
const to = row === end.y ? Math.max(from, end.x) : Number.POSITIVE_INFINITY;
|
|
1629
|
-
return highlightRow(line2, from, to);
|
|
1630
|
-
}).join(`
|
|
1631
|
-
`);
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
// ../../adapters/terminal/src/text-buffer.ts
|
|
1635
|
-
function codePoints(value) {
|
|
1636
|
-
return Array.from(value);
|
|
1637
|
-
}
|
|
1638
|
-
function codePointLength(value) {
|
|
1639
|
-
return codePoints(value).length;
|
|
1640
|
-
}
|
|
1641
|
-
function sliceCodePoints(value, start, end) {
|
|
1642
|
-
return codePoints(value).slice(start, end).join("");
|
|
1643
|
-
}
|
|
1644
|
-
function normalizeText(value) {
|
|
1645
|
-
return value.replaceAll(`\r
|
|
1646
|
-
`, `
|
|
1647
|
-
`).replaceAll("\r", `
|
|
1648
|
-
`);
|
|
1649
|
-
}
|
|
1650
|
-
function characterClass(character) {
|
|
1651
|
-
if (/\s/u.test(character))
|
|
1652
|
-
return "space";
|
|
1653
|
-
if (!/[\p{L}\p{N}\p{M}_]/u.test(character))
|
|
1654
|
-
return "punctuation";
|
|
1655
|
-
if (/\p{Script=Latin}/u.test(character))
|
|
1656
|
-
return "word:latin";
|
|
1657
|
-
if (/\p{Script=Hangul}/u.test(character))
|
|
1658
|
-
return "word:hangul";
|
|
1659
|
-
if (/\p{Script=Han}/u.test(character))
|
|
1660
|
-
return "word:han";
|
|
1661
|
-
if (/\p{Script=Hiragana}/u.test(character))
|
|
1662
|
-
return "word:hiragana";
|
|
1663
|
-
if (/\p{Script=Katakana}/u.test(character))
|
|
1664
|
-
return "word:katakana";
|
|
1665
|
-
if (/\p{Script=Cyrillic}/u.test(character))
|
|
1666
|
-
return "word:cyrillic";
|
|
1667
|
-
if (/\p{Script=Arabic}/u.test(character))
|
|
1668
|
-
return "word:arabic";
|
|
1669
|
-
return "word:other";
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
class TextBuffer {
|
|
1673
|
-
#lines = [""];
|
|
1674
|
-
#cursor = { row: 0, col: 0 };
|
|
1675
|
-
#preferredCol = null;
|
|
1676
|
-
constructor(initialText = "") {
|
|
1677
|
-
this.setText(initialText);
|
|
1678
|
-
}
|
|
1679
|
-
get text() {
|
|
1680
|
-
return this.#lines.join(`
|
|
1681
|
-
`);
|
|
1682
|
-
}
|
|
1683
|
-
get lines() {
|
|
1684
|
-
return this.#lines;
|
|
1685
|
-
}
|
|
1686
|
-
get cursor() {
|
|
1687
|
-
return { ...this.#cursor };
|
|
1688
|
-
}
|
|
1689
|
-
get isAtStart() {
|
|
1690
|
-
return this.#cursor.row === 0 && this.#cursor.col === 0;
|
|
1691
|
-
}
|
|
1692
|
-
get isAtEnd() {
|
|
1693
|
-
const lastRow = this.#lines.length - 1;
|
|
1694
|
-
return this.#cursor.row === lastRow && this.#cursor.col === codePointLength(this.#lines[lastRow]);
|
|
1695
|
-
}
|
|
1696
|
-
get isOnFirstLine() {
|
|
1697
|
-
return this.#cursor.row === 0;
|
|
1698
|
-
}
|
|
1699
|
-
get isOnLastLine() {
|
|
1700
|
-
return this.#cursor.row === this.#lines.length - 1;
|
|
1701
|
-
}
|
|
1702
|
-
setText(value, cursor = "end") {
|
|
1703
|
-
this.#lines = normalizeText(value).split(`
|
|
1704
|
-
`);
|
|
1705
|
-
if (this.#lines.length === 0)
|
|
1706
|
-
this.#lines = [""];
|
|
1707
|
-
if (cursor === "start")
|
|
1708
|
-
this.#cursor = { row: 0, col: 0 };
|
|
1709
|
-
else if (cursor === "end") {
|
|
1710
|
-
const row = this.#lines.length - 1;
|
|
1711
|
-
this.#cursor = { row, col: codePointLength(this.#lines[row]) };
|
|
1712
|
-
} else {
|
|
1713
|
-
this.#cursor = this.#clampCursor(cursor);
|
|
1714
|
-
}
|
|
1715
|
-
this.#preferredCol = null;
|
|
1716
|
-
}
|
|
1717
|
-
insert(value) {
|
|
1718
|
-
if (!value)
|
|
1719
|
-
return;
|
|
1720
|
-
this.#replaceRange(this.#cursor, this.#cursor, normalizeText(value));
|
|
1721
|
-
}
|
|
1722
|
-
backspace() {
|
|
1723
|
-
if (this.isAtStart)
|
|
1724
|
-
return false;
|
|
1725
|
-
const end = this.cursor;
|
|
1726
|
-
const start = this.cursor;
|
|
1727
|
-
if (start.col > 0)
|
|
1728
|
-
start.col -= 1;
|
|
1729
|
-
else {
|
|
1730
|
-
start.row -= 1;
|
|
1731
|
-
start.col = codePointLength(this.#lines[start.row]);
|
|
1732
|
-
}
|
|
1733
|
-
this.#replaceRange(start, end, "");
|
|
1734
|
-
return true;
|
|
1735
|
-
}
|
|
1736
|
-
deleteForward() {
|
|
1737
|
-
if (this.isAtEnd)
|
|
1738
|
-
return false;
|
|
1739
|
-
const start = this.cursor;
|
|
1740
|
-
const end = this.cursor;
|
|
1741
|
-
const lineLength = codePointLength(this.#lines[end.row]);
|
|
1742
|
-
if (end.col < lineLength)
|
|
1743
|
-
end.col += 1;
|
|
1744
|
-
else {
|
|
1745
|
-
end.row += 1;
|
|
1746
|
-
end.col = 0;
|
|
1747
|
-
}
|
|
1748
|
-
this.#replaceRange(start, end, "");
|
|
1749
|
-
return true;
|
|
1750
|
-
}
|
|
1751
|
-
deleteWordLeft() {
|
|
1752
|
-
if (this.isAtStart)
|
|
1753
|
-
return false;
|
|
1754
|
-
const end = this.cursor;
|
|
1755
|
-
this.moveWordLeft();
|
|
1756
|
-
this.#replaceRange(this.cursor, end, "");
|
|
1757
|
-
return true;
|
|
1758
|
-
}
|
|
1759
|
-
clearBeforeCursor() {
|
|
1760
|
-
this.#replaceRange({ row: 0, col: 0 }, this.cursor, "");
|
|
1761
|
-
}
|
|
1762
|
-
clearAfterCursor() {
|
|
1763
|
-
const row = this.#lines.length - 1;
|
|
1764
|
-
this.#replaceRange(this.cursor, { row, col: codePointLength(this.#lines[row]) }, "");
|
|
1765
|
-
}
|
|
1766
|
-
moveLeft() {
|
|
1767
|
-
if (this.#cursor.col > 0)
|
|
1768
|
-
this.#cursor.col -= 1;
|
|
1769
|
-
else if (this.#cursor.row > 0) {
|
|
1770
|
-
this.#cursor.row -= 1;
|
|
1771
|
-
this.#cursor.col = codePointLength(this.#lines[this.#cursor.row]);
|
|
1772
|
-
}
|
|
1773
|
-
this.#preferredCol = null;
|
|
1774
|
-
}
|
|
1775
|
-
moveRight() {
|
|
1776
|
-
const lineLength = codePointLength(this.#lines[this.#cursor.row]);
|
|
1777
|
-
if (this.#cursor.col < lineLength)
|
|
1778
|
-
this.#cursor.col += 1;
|
|
1779
|
-
else if (this.#cursor.row < this.#lines.length - 1) {
|
|
1780
|
-
this.#cursor.row += 1;
|
|
1781
|
-
this.#cursor.col = 0;
|
|
1782
|
-
}
|
|
1783
|
-
this.#preferredCol = null;
|
|
1784
|
-
}
|
|
1785
|
-
moveUp() {
|
|
1786
|
-
if (this.#cursor.row === 0)
|
|
1787
|
-
return;
|
|
1788
|
-
const preferred = this.#preferredCol ?? this.#cursor.col;
|
|
1789
|
-
this.#preferredCol = preferred;
|
|
1790
|
-
this.#cursor.row -= 1;
|
|
1791
|
-
this.#cursor.col = Math.min(preferred, codePointLength(this.#lines[this.#cursor.row]));
|
|
1792
|
-
}
|
|
1793
|
-
moveDown() {
|
|
1794
|
-
if (this.#cursor.row >= this.#lines.length - 1)
|
|
1795
|
-
return;
|
|
1796
|
-
const preferred = this.#preferredCol ?? this.#cursor.col;
|
|
1797
|
-
this.#preferredCol = preferred;
|
|
1798
|
-
this.#cursor.row += 1;
|
|
1799
|
-
this.#cursor.col = Math.min(preferred, codePointLength(this.#lines[this.#cursor.row]));
|
|
1800
|
-
}
|
|
1801
|
-
moveHome() {
|
|
1802
|
-
this.#cursor.col = 0;
|
|
1803
|
-
this.#preferredCol = null;
|
|
1804
|
-
}
|
|
1805
|
-
moveEnd() {
|
|
1806
|
-
this.#cursor.col = codePointLength(this.#lines[this.#cursor.row]);
|
|
1807
|
-
this.#preferredCol = null;
|
|
1808
|
-
}
|
|
1809
|
-
moveWordLeft() {
|
|
1810
|
-
if (this.#cursor.col === 0) {
|
|
1811
|
-
if (this.#cursor.row === 0)
|
|
1812
|
-
return;
|
|
1813
|
-
this.#cursor.row -= 1;
|
|
1814
|
-
this.#cursor.col = codePointLength(this.#lines[this.#cursor.row]);
|
|
1815
|
-
}
|
|
1816
|
-
const characters = codePoints(this.#lines[this.#cursor.row]);
|
|
1817
|
-
let col = this.#cursor.col;
|
|
1818
|
-
while (col > 0 && characterClass(characters[col - 1]) === "space")
|
|
1819
|
-
col -= 1;
|
|
1820
|
-
if (col > 0) {
|
|
1821
|
-
const targetClass = characterClass(characters[col - 1]);
|
|
1822
|
-
while (col > 0 && characterClass(characters[col - 1]) === targetClass)
|
|
1823
|
-
col -= 1;
|
|
1824
|
-
}
|
|
1825
|
-
this.#cursor.col = col;
|
|
1826
|
-
this.#preferredCol = null;
|
|
1827
|
-
}
|
|
1828
|
-
moveWordRight() {
|
|
1829
|
-
const characters = codePoints(this.#lines[this.#cursor.row]);
|
|
1830
|
-
let col = this.#cursor.col;
|
|
1831
|
-
if (col >= characters.length) {
|
|
1832
|
-
if (this.#cursor.row >= this.#lines.length - 1)
|
|
1833
|
-
return;
|
|
1834
|
-
this.#cursor.row += 1;
|
|
1835
|
-
this.#cursor.col = 0;
|
|
1836
|
-
this.#preferredCol = null;
|
|
1837
|
-
return;
|
|
1838
|
-
}
|
|
1839
|
-
const targetClass = characterClass(characters[col]);
|
|
1840
|
-
while (col < characters.length && characterClass(characters[col]) === targetClass)
|
|
1841
|
-
col += 1;
|
|
1842
|
-
while (col < characters.length && characterClass(characters[col]) === "space")
|
|
1843
|
-
col += 1;
|
|
1844
|
-
this.#cursor.col = col;
|
|
1845
|
-
this.#preferredCol = null;
|
|
1846
|
-
}
|
|
1847
|
-
#clampCursor(cursor) {
|
|
1848
|
-
const row = Math.max(0, Math.min(cursor.row, this.#lines.length - 1));
|
|
1849
|
-
return {
|
|
1850
|
-
row,
|
|
1851
|
-
col: Math.max(0, Math.min(cursor.col, codePointLength(this.#lines[row])))
|
|
1852
|
-
};
|
|
1853
|
-
}
|
|
1854
|
-
#replaceRange(startInput, endInput, replacement) {
|
|
1855
|
-
const start = this.#clampCursor(startInput);
|
|
1856
|
-
const end = this.#clampCursor(endInput);
|
|
1857
|
-
if (start.row > end.row || start.row === end.row && start.col > end.col)
|
|
1858
|
-
return;
|
|
1859
|
-
const prefix = sliceCodePoints(this.#lines[start.row], 0, start.col);
|
|
1860
|
-
const suffix = sliceCodePoints(this.#lines[end.row], end.col);
|
|
1861
|
-
const parts = normalizeText(replacement).split(`
|
|
1862
|
-
`);
|
|
1863
|
-
if (parts.length === 1) {
|
|
1864
|
-
this.#lines.splice(start.row, end.row - start.row + 1, `${prefix}${parts[0]}${suffix}`);
|
|
1865
|
-
this.#cursor = { row: start.row, col: start.col + codePointLength(parts[0]) };
|
|
1866
|
-
} else {
|
|
1867
|
-
const first = `${prefix}${parts[0]}`;
|
|
1868
|
-
const lastPart = parts.at(-1) ?? "";
|
|
1869
|
-
const last = `${lastPart}${suffix}`;
|
|
1870
|
-
this.#lines.splice(start.row, end.row - start.row + 1, first, ...parts.slice(1, -1), last);
|
|
1871
|
-
this.#cursor = {
|
|
1872
|
-
row: start.row + parts.length - 1,
|
|
1873
|
-
col: codePointLength(lastPart)
|
|
1874
|
-
};
|
|
1875
|
-
}
|
|
1876
|
-
this.#preferredCol = null;
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
class InputHistory {
|
|
1881
|
-
maxEntries;
|
|
1882
|
-
#entries;
|
|
1883
|
-
#index;
|
|
1884
|
-
#draft = "";
|
|
1885
|
-
constructor(entries = [], maxEntries = 200) {
|
|
1886
|
-
this.maxEntries = maxEntries;
|
|
1887
|
-
this.#entries = entries.filter(Boolean).slice(-maxEntries);
|
|
1888
|
-
this.#index = this.#entries.length;
|
|
1889
|
-
}
|
|
1890
|
-
get entries() {
|
|
1891
|
-
return this.#entries;
|
|
1892
|
-
}
|
|
1893
|
-
record(value) {
|
|
1894
|
-
const text = value.trim();
|
|
1895
|
-
if (!text)
|
|
1896
|
-
return;
|
|
1897
|
-
if (this.#entries.at(-1) !== text)
|
|
1898
|
-
this.#entries.push(text);
|
|
1899
|
-
this.#entries = this.#entries.slice(-this.maxEntries);
|
|
1900
|
-
this.reset();
|
|
1901
|
-
}
|
|
1902
|
-
previous(currentDraft) {
|
|
1903
|
-
if (this.#entries.length === 0 || this.#index === 0)
|
|
1904
|
-
return null;
|
|
1905
|
-
if (this.#index === this.#entries.length)
|
|
1906
|
-
this.#draft = currentDraft;
|
|
1907
|
-
this.#index -= 1;
|
|
1908
|
-
return this.#entries[this.#index] ?? null;
|
|
1909
|
-
}
|
|
1910
|
-
next() {
|
|
1911
|
-
if (this.#index >= this.#entries.length)
|
|
1912
|
-
return null;
|
|
1913
|
-
this.#index += 1;
|
|
1914
|
-
return this.#index === this.#entries.length ? this.#draft : this.#entries[this.#index] ?? "";
|
|
1915
|
-
}
|
|
1916
|
-
reset() {
|
|
1917
|
-
this.#index = this.#entries.length;
|
|
1918
|
-
this.#draft = "";
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
// ../../adapters/terminal/src/app.ts
|
|
1923
|
-
var ENTER_ALT_SCREEN = "\x1B]11;#0a0b0f\x07\x1B[?1049h\x1B[48;2;10;11;15m\x1B[2J\x1B[H\x1B[?25l\x1B[?2004h\x1B[?1002h\x1B[?1006h";
|
|
1924
|
-
var EXIT_ALT_SCREEN = "\x1B[0m\x1B[?1006l\x1B[?1002l\x1B[?2004l\x1B[?25h\x1B[?1049l\x1B]111\x07";
|
|
1925
|
-
var NEW_TOPIC_KEYS = new Set(["n", "\u315C"]);
|
|
1926
|
-
var DELETE_TOPIC_KEYS = new Set(["d", "\u3147", "\x7F", "\b", "\x1B[3~"]);
|
|
1927
|
-
var CONFIRM_KEYS = new Set(["y", "\u315B"]);
|
|
1928
|
-
var CANCEL_KEYS = new Set(["n", "\u315C"]);
|
|
1929
|
-
var MESSAGE_MUTATING_AI_STATUS_KINDS = new Set(["tool_call", "tool_output"]);
|
|
1930
|
-
var SGR_MOUSE_PATTERN = new RegExp("\\u001b\\[<(\\d+);(\\d+);(\\d+)([mM])", "g");
|
|
1931
|
-
function selectableEfforts(topic) {
|
|
1932
|
-
return topic?.agent ? getRegistry(topic.agent).validEfforts : EFFORT_VALUES;
|
|
1933
|
-
}
|
|
1934
|
-
function vaultFormBlocksOverlaySwitch(state) {
|
|
1935
|
-
return state.overlay === "vault" && (state.vaultMode === "value" || state.vaultMode === "description");
|
|
1936
|
-
}
|
|
1937
|
-
function runtimeEventWaitsForMessageLoad(event) {
|
|
1938
|
-
if (event.type === "message" || event.type === "message-updated")
|
|
1939
|
-
return true;
|
|
1940
|
-
if (event.type !== "ai-status")
|
|
1941
|
-
return false;
|
|
1942
|
-
const payload = event.payload;
|
|
1943
|
-
return typeof payload?.kind === "string" && MESSAGE_MUTATING_AI_STATUS_KINDS.has(payload.kind);
|
|
1944
|
-
}
|
|
1945
|
-
function animationFrameAt(nowMs = terminalNowMs()) {
|
|
1946
|
-
return Math.floor(nowMs / WORKING_FRAME_INTERVAL_MS);
|
|
1947
|
-
}
|
|
1948
|
-
function consumeMouseInput(raw) {
|
|
1949
|
-
let scrollDelta = 0;
|
|
1950
|
-
const events = [];
|
|
1951
|
-
const input = raw.replace(SGR_MOUSE_PATTERN, (_sequence, rawButton, rawX, rawY, suffix) => {
|
|
1952
|
-
const button = Number.parseInt(rawButton, 10);
|
|
1953
|
-
if (Number.isFinite(button) && (button & 64) !== 0) {
|
|
1954
|
-
scrollDelta += (button & 1) === 0 ? 3 : -3;
|
|
1955
|
-
} else {
|
|
1956
|
-
const x = Number.parseInt(rawX, 10);
|
|
1957
|
-
const y = Number.parseInt(rawY, 10);
|
|
1958
|
-
if (Number.isFinite(button) && Number.isFinite(x) && Number.isFinite(y)) {
|
|
1959
|
-
events.push({
|
|
1960
|
-
button,
|
|
1961
|
-
x,
|
|
1962
|
-
y,
|
|
1963
|
-
kind: suffix === "m" ? "release" : (button & 32) !== 0 ? "drag" : "press"
|
|
1964
|
-
});
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
return "";
|
|
1968
|
-
});
|
|
1969
|
-
return { input, scrollDelta, events };
|
|
1970
|
-
}
|
|
1971
|
-
function escapeStopsActiveTurn(state) {
|
|
1972
|
-
if (state.overlay || state.creatingTopic)
|
|
1973
|
-
return false;
|
|
1974
|
-
const topic = activeTopic(state);
|
|
1975
|
-
return Boolean(topic && state.activity[topic.id]?.running);
|
|
1976
|
-
}
|
|
1977
|
-
function ctrlCExitsTopicPicker(state) {
|
|
1978
|
-
return state.overlay === "topics";
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
class TerminalApp {
|
|
1982
|
-
#client;
|
|
1983
|
-
#options;
|
|
1984
|
-
#state;
|
|
1985
|
-
#input = new TextBuffer;
|
|
1986
|
-
#screen = new TerminalScreenRenderer;
|
|
1987
|
-
#history = new InputHistory;
|
|
1988
|
-
#vaultDraftValue = "";
|
|
1989
|
-
#pasting = false;
|
|
1990
|
-
#renderQueued = false;
|
|
1991
|
-
#renderTimer;
|
|
1992
|
-
#animationTimer;
|
|
1993
|
-
#backgroundRefreshTimer;
|
|
1994
|
-
#backgroundRefreshRunning = false;
|
|
1995
|
-
#animationFrame = animationFrameAt();
|
|
1996
|
-
#topicsRefreshGeneration = 0;
|
|
1997
|
-
#messageLoadGeneration = new Map;
|
|
1998
|
-
#messageHistory = new Map;
|
|
1999
|
-
#queuedRuntimeEvents = new Map;
|
|
2000
|
-
#selection = null;
|
|
2001
|
-
#plainFrameLines = [];
|
|
2002
|
-
#lastInterruptAt = 0;
|
|
2003
|
-
#running = false;
|
|
2004
|
-
#stopRequested = false;
|
|
2005
|
-
#finishRun = null;
|
|
2006
|
-
#onData = (chunk) => this.#handleInput(String(chunk));
|
|
2007
|
-
#onResize = () => {
|
|
2008
|
-
this.#selection = null;
|
|
2009
|
-
this.#screen.invalidate();
|
|
2010
|
-
this.#queueRender();
|
|
2011
|
-
};
|
|
2012
|
-
#onSignal = () => this.#requestExit();
|
|
2013
|
-
constructor(client, options) {
|
|
2014
|
-
this.#client = client;
|
|
2015
|
-
this.#options = options;
|
|
2016
|
-
this.#state = createInitialState(options.userId);
|
|
2017
|
-
}
|
|
2018
|
-
async run() {
|
|
2019
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2020
|
-
throw new Error("terminal-adapter requires an interactive TTY");
|
|
2021
|
-
}
|
|
2022
|
-
let clientStartAttempted = false;
|
|
2023
|
-
let uiActive = false;
|
|
2024
|
-
try {
|
|
2025
|
-
clientStartAttempted = true;
|
|
2026
|
-
await this.#client.start((event) => this.#handleRuntimeEvent(event));
|
|
2027
|
-
this.#history = new InputHistory(this.#client.listInputHistory?.() ?? []);
|
|
2028
|
-
if (this.#stopRequested)
|
|
2029
|
-
return;
|
|
2030
|
-
await this.#refreshTopics(this.#options.preferredTopic ?? "General");
|
|
2031
|
-
await this.#refreshBackgroundSessions();
|
|
2032
|
-
if (this.#options.preferredTopic) {
|
|
2033
|
-
const preferred = this.#state.topics.find((topic) => topic.title.toLowerCase() === this.#options.preferredTopic?.toLowerCase());
|
|
2034
|
-
if (!preferred) {
|
|
2035
|
-
const created = await this.#client.createTopic(this.#options.preferredTopic, this.#options.defaultAgent);
|
|
2036
|
-
this.#state = focusCreatedTopic(this.#state, created);
|
|
2037
|
-
await this.#refreshTopics(created.title);
|
|
2038
|
-
this.#state = selectTopic(this.#state, created.id);
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
if (!this.#options.preferredTopic) {
|
|
2042
|
-
this.#state = openTopicPicker(this.#state, this.#state.notice, true);
|
|
2043
|
-
} else {
|
|
2044
|
-
await this.#loadActiveMessages();
|
|
2045
|
-
}
|
|
2046
|
-
this.#running = true;
|
|
2047
|
-
process.stdout.write(ENTER_ALT_SCREEN);
|
|
2048
|
-
uiActive = true;
|
|
2049
|
-
process.stdin.setEncoding("utf8");
|
|
2050
|
-
process.stdin.setRawMode(true);
|
|
2051
|
-
process.stdin.resume();
|
|
2052
|
-
process.stdin.on("data", this.#onData);
|
|
2053
|
-
process.stdout.on("resize", this.#onResize);
|
|
2054
|
-
process.once("SIGINT", this.#onSignal);
|
|
2055
|
-
process.once("SIGTERM", this.#onSignal);
|
|
2056
|
-
this.#render();
|
|
2057
|
-
this.#animationTimer = setInterval(() => {
|
|
2058
|
-
const activeRunning = Boolean(this.#state.activeTopicId && this.#state.activity[this.#state.activeTopicId]?.running);
|
|
2059
|
-
const pickerRunning = this.#state.overlay === "topics" && this.#state.topics.some((topic) => this.#state.activity[topic.id]?.running);
|
|
2060
|
-
if (!activeRunning && !pickerRunning && this.#state.backgroundSessions.length === 0)
|
|
2061
|
-
return;
|
|
2062
|
-
this.#animationFrame = animationFrameAt();
|
|
2063
|
-
this.#queueRender();
|
|
2064
|
-
}, WORKING_FRAME_INTERVAL_MS);
|
|
2065
|
-
this.#backgroundRefreshTimer = setInterval(() => {
|
|
2066
|
-
this.#refreshBackgroundSessions();
|
|
2067
|
-
}, 1000);
|
|
2068
|
-
this.#backgroundRefreshTimer.unref?.();
|
|
2069
|
-
await new Promise((resolve) => {
|
|
2070
|
-
this.#finishRun = resolve;
|
|
2071
|
-
});
|
|
2072
|
-
} finally {
|
|
2073
|
-
this.#running = false;
|
|
2074
|
-
await this.#cleanup(clientStartAttempted, uiActive);
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
stop() {
|
|
2078
|
-
this.#stopRequested = true;
|
|
2079
|
-
this.#requestExit();
|
|
2080
|
-
}
|
|
2081
|
-
#handleRuntimeEvent(event) {
|
|
2082
|
-
this.#selection = null;
|
|
2083
|
-
if (this.#messageLoadGeneration.has(event.topicId) && runtimeEventWaitsForMessageLoad(event)) {
|
|
2084
|
-
const queued = this.#queuedRuntimeEvents.get(event.topicId) ?? [];
|
|
2085
|
-
queued.push(event);
|
|
2086
|
-
this.#queuedRuntimeEvents.set(event.topicId, queued);
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
|
-
this.#state = applyRuntimeEvent(this.#state, event);
|
|
2090
|
-
if (event.type === "topic-created" || event.type === "topic-updated" || event.type === "topic-deleted") {
|
|
2091
|
-
const previous = this.#state.activeTopicId;
|
|
2092
|
-
this.#refreshTopicsAfterEvent(previous);
|
|
2093
|
-
}
|
|
2094
|
-
this.#queueRender();
|
|
2095
|
-
}
|
|
2096
|
-
async#refreshTopicsAfterEvent(previous) {
|
|
2097
|
-
try {
|
|
2098
|
-
await this.#refreshTopics();
|
|
2099
|
-
if (this.#state.activeTopicId && this.#state.activeTopicId !== previous) {
|
|
2100
|
-
await this.#loadActiveMessages();
|
|
2101
|
-
}
|
|
2102
|
-
} catch (error) {
|
|
2103
|
-
this.#state = {
|
|
2104
|
-
...this.#state,
|
|
2105
|
-
notice: `Node connection error: ${error instanceof Error ? error.message : String(error)}`
|
|
2106
|
-
};
|
|
2107
|
-
}
|
|
2108
|
-
this.#queueRender();
|
|
2109
|
-
}
|
|
2110
|
-
async#refreshTopics(preferredTitle) {
|
|
2111
|
-
const generation = ++this.#topicsRefreshGeneration;
|
|
2112
|
-
const topics = await this.#client.listTopics();
|
|
2113
|
-
if (generation !== this.#topicsRefreshGeneration)
|
|
2114
|
-
return;
|
|
2115
|
-
this.#state = setTopics(this.#state, topics, preferredTitle);
|
|
2116
|
-
}
|
|
2117
|
-
async#refreshBackgroundSessions() {
|
|
2118
|
-
if (!this.#client.listBackgroundSessions || this.#backgroundRefreshRunning)
|
|
2119
|
-
return;
|
|
2120
|
-
this.#backgroundRefreshRunning = true;
|
|
2121
|
-
try {
|
|
2122
|
-
const sessions = await this.#client.listBackgroundSessions();
|
|
2123
|
-
const before = JSON.stringify(this.#state.backgroundSessions);
|
|
2124
|
-
this.#state = setBackgroundSessions(this.#state, sessions);
|
|
2125
|
-
if (JSON.stringify(this.#state.backgroundSessions) !== before)
|
|
2126
|
-
this.#queueRender();
|
|
2127
|
-
} catch {} finally {
|
|
2128
|
-
this.#backgroundRefreshRunning = false;
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
async#loadActiveMessages() {
|
|
2132
|
-
const topic = activeTopic(this.#state);
|
|
2133
|
-
if (!topic)
|
|
2134
|
-
return;
|
|
2135
|
-
const generation = (this.#messageLoadGeneration.get(topic.id) ?? 0) + 1;
|
|
2136
|
-
this.#messageLoadGeneration.set(topic.id, generation);
|
|
2137
|
-
let messagePage;
|
|
2138
|
-
let recentEvents;
|
|
2139
|
-
try {
|
|
2140
|
-
const messageRequest = this.#client.listMessagePage ? Promise.resolve(this.#client.listMessagePage(topic.id, undefined, INITIAL_MESSAGE_HISTORY_LIMIT)) : Promise.resolve(this.#client.listMessages(topic.id)).then((messages) => ({
|
|
2141
|
-
messages,
|
|
2142
|
-
hasMore: false
|
|
2143
|
-
}));
|
|
2144
|
-
[messagePage, recentEvents] = await Promise.all([
|
|
2145
|
-
messageRequest,
|
|
2146
|
-
this.#client.listRecentEvents?.(topic.id) ?? []
|
|
2147
|
-
]);
|
|
2148
|
-
} catch (error) {
|
|
2149
|
-
if (this.#messageLoadGeneration.get(topic.id) === generation) {
|
|
2150
|
-
this.#messageLoadGeneration.delete(topic.id);
|
|
2151
|
-
const queued2 = this.#queuedRuntimeEvents.get(topic.id) ?? [];
|
|
2152
|
-
this.#queuedRuntimeEvents.delete(topic.id);
|
|
2153
|
-
for (const event of queued2)
|
|
2154
|
-
this.#state = applyRuntimeEvent(this.#state, event);
|
|
2155
|
-
}
|
|
2156
|
-
throw error;
|
|
2157
|
-
}
|
|
2158
|
-
if (this.#messageLoadGeneration.get(topic.id) !== generation)
|
|
2159
|
-
return;
|
|
2160
|
-
this.#state = setMessages(this.#state, topic.id, messagePage.messages);
|
|
2161
|
-
this.#messageHistory.set(topic.id, {
|
|
2162
|
-
cursor: messagePage.cursor,
|
|
2163
|
-
hasMore: messagePage.hasMore,
|
|
2164
|
-
loading: false
|
|
2165
|
-
});
|
|
2166
|
-
this.#state = setMessageHistoryStatus(this.#state, topic.id, {
|
|
2167
|
-
hasMore: messagePage.hasMore,
|
|
2168
|
-
loading: false
|
|
2169
|
-
});
|
|
2170
|
-
for (const event of recentEvents) {
|
|
2171
|
-
this.#state = applyRuntimeEvent(this.#state, event);
|
|
2172
|
-
}
|
|
2173
|
-
this.#messageLoadGeneration.delete(topic.id);
|
|
2174
|
-
const queued = this.#queuedRuntimeEvents.get(topic.id) ?? [];
|
|
2175
|
-
this.#queuedRuntimeEvents.delete(topic.id);
|
|
2176
|
-
for (const event of queued)
|
|
2177
|
-
this.#state = applyRuntimeEvent(this.#state, event);
|
|
2178
|
-
}
|
|
2179
|
-
async#loadOlderMessages(topicId, targetOffset) {
|
|
2180
|
-
const history = this.#messageHistory.get(topicId);
|
|
2181
|
-
if (!this.#client.listMessagePage || !history?.hasMore || history.loading)
|
|
2182
|
-
return;
|
|
2183
|
-
const cursor = history.cursor;
|
|
2184
|
-
if (!cursor)
|
|
2185
|
-
return;
|
|
2186
|
-
this.#messageHistory.set(topicId, { ...history, loading: true });
|
|
2187
|
-
this.#state = setMessageHistoryStatus(this.#state, topicId, {
|
|
2188
|
-
hasMore: history.hasMore,
|
|
2189
|
-
loading: true
|
|
2190
|
-
});
|
|
2191
|
-
this.#queueRender();
|
|
2192
|
-
try {
|
|
2193
|
-
const page = await this.#client.listMessagePage(topicId, cursor, MESSAGE_HISTORY_PAGE_SIZE);
|
|
2194
|
-
const latestHistory = this.#messageHistory.get(topicId);
|
|
2195
|
-
if (!latestHistory || latestHistory.cursor !== cursor)
|
|
2196
|
-
return;
|
|
2197
|
-
const current = this.#state.messages[topicId] ?? [];
|
|
2198
|
-
const currentIds = new Set(current.map((message) => message.id));
|
|
2199
|
-
const older = page.messages.filter((message) => !currentIds.has(message.id));
|
|
2200
|
-
this.#state = setMessages(this.#state, topicId, [...older, ...current]);
|
|
2201
|
-
this.#messageHistory.set(topicId, {
|
|
2202
|
-
cursor: page.cursor,
|
|
2203
|
-
hasMore: page.hasMore,
|
|
2204
|
-
loading: false
|
|
2205
|
-
});
|
|
2206
|
-
this.#state = setMessageHistoryStatus(this.#state, topicId, {
|
|
2207
|
-
hasMore: page.hasMore,
|
|
2208
|
-
loading: false
|
|
2209
|
-
});
|
|
2210
|
-
if (this.#state.activeTopicId === topicId) {
|
|
2211
|
-
const maxOffset = maxConversationScrollOffset(this.#state, process.stdout.columns ?? 100, process.stdout.rows ?? 30);
|
|
2212
|
-
this.#state = {
|
|
2213
|
-
...this.#state,
|
|
2214
|
-
scrollOffset: Math.min(maxOffset, Math.max(0, targetOffset)),
|
|
2215
|
-
notice: older.length === 0 || !page.hasMore ? "Start of conversation" : `Loaded ${older.length} older messages`
|
|
2216
|
-
};
|
|
2217
|
-
}
|
|
2218
|
-
} catch (error) {
|
|
2219
|
-
const latestHistory = this.#messageHistory.get(topicId);
|
|
2220
|
-
if (latestHistory?.cursor === cursor) {
|
|
2221
|
-
this.#messageHistory.set(topicId, { ...latestHistory, loading: false });
|
|
2222
|
-
this.#state = setMessageHistoryStatus(this.#state, topicId, {
|
|
2223
|
-
hasMore: latestHistory.hasMore,
|
|
2224
|
-
loading: false
|
|
2225
|
-
});
|
|
2226
|
-
}
|
|
2227
|
-
this.#state = {
|
|
2228
|
-
...this.#state,
|
|
2229
|
-
notice: `History load failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2230
|
-
};
|
|
2231
|
-
}
|
|
2232
|
-
this.#queueRender();
|
|
2233
|
-
}
|
|
2234
|
-
#queueRender() {
|
|
2235
|
-
if (!this.#running || this.#renderQueued)
|
|
2236
|
-
return;
|
|
2237
|
-
this.#renderQueued = true;
|
|
2238
|
-
this.#renderTimer = setTimeout(() => {
|
|
2239
|
-
this.#renderTimer = undefined;
|
|
2240
|
-
this.#renderQueued = false;
|
|
2241
|
-
if (this.#running)
|
|
2242
|
-
this.#render();
|
|
2243
|
-
}, 16);
|
|
2244
|
-
}
|
|
2245
|
-
#render() {
|
|
2246
|
-
const columns = process.stdout.columns ?? 100;
|
|
2247
|
-
const rows = process.stdout.rows ?? 30;
|
|
2248
|
-
const rendered = renderAppFrame(this.#state, columns, rows, this.#animationFrame);
|
|
2249
|
-
const baseFrame = rendered.frame;
|
|
2250
|
-
this.#plainFrameLines = stripAnsi(baseFrame).split(`
|
|
2251
|
-
`);
|
|
2252
|
-
const frame = this.#selection ? highlightScreenSelection(baseFrame, this.#selection) : baseFrame;
|
|
2253
|
-
const patch = this.#screen.update(frame);
|
|
2254
|
-
const cursor = rendered.cursor ? placeTerminalCursor(rendered.cursor) : "";
|
|
2255
|
-
if (patch || cursor)
|
|
2256
|
-
process.stdout.write(`${patch}${cursor}`);
|
|
2257
|
-
}
|
|
2258
|
-
#syncInput() {
|
|
2259
|
-
const count = this.#suggestionCount();
|
|
2260
|
-
this.#state = {
|
|
2261
|
-
...this.#state,
|
|
2262
|
-
input: this.#input.text,
|
|
2263
|
-
inputCursor: this.#input.cursor,
|
|
2264
|
-
suggestionIndex: count === 0 ? 0 : Math.min(this.#state.suggestionIndex, count - 1)
|
|
2265
|
-
};
|
|
2266
|
-
}
|
|
2267
|
-
#replaceInput(value) {
|
|
2268
|
-
this.#input.setText(value);
|
|
2269
|
-
this.#syncInput();
|
|
2270
|
-
this.#queueRender();
|
|
2271
|
-
}
|
|
2272
|
-
#handleInput(raw) {
|
|
2273
|
-
if (!this.#running)
|
|
2274
|
-
return;
|
|
2275
|
-
const mouse = consumeMouseInput(raw);
|
|
2276
|
-
if (mouse.scrollDelta !== 0) {
|
|
2277
|
-
this.#selection = null;
|
|
2278
|
-
this.#scroll(mouse.scrollDelta);
|
|
2279
|
-
}
|
|
2280
|
-
for (const event of mouse.events)
|
|
2281
|
-
this.#handleMouseSelection(event);
|
|
2282
|
-
let chunk = mouse.input;
|
|
2283
|
-
if (!chunk)
|
|
2284
|
-
return;
|
|
2285
|
-
this.#selection = null;
|
|
2286
|
-
const pasteStart = "\x1B[200~";
|
|
2287
|
-
const pasteEnd = "\x1B[201~";
|
|
2288
|
-
if (this.#pasting) {
|
|
2289
|
-
const end = chunk.indexOf(pasteEnd);
|
|
2290
|
-
if (end < 0) {
|
|
2291
|
-
this.#input.insert(chunk);
|
|
2292
|
-
this.#syncInput();
|
|
2293
|
-
this.#queueRender();
|
|
2294
|
-
return;
|
|
2295
|
-
}
|
|
2296
|
-
this.#input.insert(chunk.slice(0, end));
|
|
2297
|
-
this.#pasting = false;
|
|
2298
|
-
chunk = chunk.slice(end + pasteEnd.length);
|
|
2299
|
-
this.#syncInput();
|
|
2300
|
-
if (!chunk) {
|
|
2301
|
-
this.#queueRender();
|
|
2302
|
-
return;
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
const start = chunk.indexOf(pasteStart);
|
|
2306
|
-
if (start >= 0) {
|
|
2307
|
-
const before = chunk.slice(0, start);
|
|
2308
|
-
if (before)
|
|
2309
|
-
this.#handleInput(before);
|
|
2310
|
-
this.#pasting = true;
|
|
2311
|
-
this.#handleInput(chunk.slice(start + pasteStart.length));
|
|
2312
|
-
return;
|
|
2313
|
-
}
|
|
2314
|
-
if (chunk === "\x03") {
|
|
2315
|
-
this.#handleInterrupt();
|
|
2316
|
-
return;
|
|
2317
|
-
}
|
|
2318
|
-
if (this.#state.overlay === "topics" && this.#state.topicPickerRoot) {
|
|
2319
|
-
this.#lastInterruptAt = 0;
|
|
2320
|
-
this.#handleTopicPickerInput(chunk);
|
|
2321
|
-
return;
|
|
2322
|
-
}
|
|
2323
|
-
this.#lastInterruptAt = 0;
|
|
2324
|
-
const editingVaultSecret = vaultFormBlocksOverlaySwitch(this.#state);
|
|
2325
|
-
if (chunk === "\x18") {
|
|
2326
|
-
this.#abort();
|
|
2327
|
-
return;
|
|
2328
|
-
}
|
|
2329
|
-
if (chunk === "\x10") {
|
|
2330
|
-
this.#cycleTopic(-1);
|
|
2331
|
-
return;
|
|
2332
|
-
}
|
|
2333
|
-
if (chunk === "\x0E") {
|
|
2334
|
-
this.#cycleTopic(1);
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
if (chunk === "\x0F") {
|
|
2338
|
-
if (editingVaultSecret)
|
|
2339
|
-
return;
|
|
2340
|
-
this.#toggleTopics();
|
|
2341
|
-
return;
|
|
2342
|
-
}
|
|
2343
|
-
if (chunk === "\x14") {
|
|
2344
|
-
if (editingVaultSecret)
|
|
2345
|
-
return;
|
|
2346
|
-
this.#state = {
|
|
2347
|
-
...this.#state,
|
|
2348
|
-
overlay: this.#state.overlay === "transcript" ? null : "transcript"
|
|
2349
|
-
};
|
|
2350
|
-
this.#queueRender();
|
|
2351
|
-
return;
|
|
2352
|
-
}
|
|
2353
|
-
if (chunk === "\x19") {
|
|
2354
|
-
this.#copy(false);
|
|
2355
|
-
return;
|
|
2356
|
-
}
|
|
2357
|
-
if (chunk === "\x05") {
|
|
2358
|
-
this.#loadOlderHistory();
|
|
2359
|
-
return;
|
|
2360
|
-
}
|
|
2361
|
-
if (chunk === "\f") {
|
|
2362
|
-
this.#screen.invalidate();
|
|
2363
|
-
this.#render();
|
|
2364
|
-
return;
|
|
2365
|
-
}
|
|
2366
|
-
if (chunk === "\x01" || chunk === "\x1B[H" || chunk === "\x1B[1~") {
|
|
2367
|
-
this.#input.moveHome();
|
|
2368
|
-
this.#syncInput();
|
|
2369
|
-
this.#queueRender();
|
|
2370
|
-
return;
|
|
2371
|
-
}
|
|
2372
|
-
if (chunk === "\x1B[F" || chunk === "\x1B[4~") {
|
|
2373
|
-
this.#input.moveEnd();
|
|
2374
|
-
this.#syncInput();
|
|
2375
|
-
this.#queueRender();
|
|
2376
|
-
return;
|
|
2377
|
-
}
|
|
2378
|
-
if (chunk === "\x1B[D" || chunk === "\x02") {
|
|
2379
|
-
this.#input.moveLeft();
|
|
2380
|
-
this.#syncInput();
|
|
2381
|
-
this.#queueRender();
|
|
2382
|
-
return;
|
|
2383
|
-
}
|
|
2384
|
-
if (chunk === "\x1B[C" || chunk === "\x06") {
|
|
2385
|
-
this.#input.moveRight();
|
|
2386
|
-
this.#syncInput();
|
|
2387
|
-
this.#queueRender();
|
|
2388
|
-
return;
|
|
2389
|
-
}
|
|
2390
|
-
if (chunk === "\x1Bb" || chunk === "\x1B[1;5D") {
|
|
2391
|
-
this.#input.moveWordLeft();
|
|
2392
|
-
this.#syncInput();
|
|
2393
|
-
this.#queueRender();
|
|
2394
|
-
return;
|
|
2395
|
-
}
|
|
2396
|
-
if (chunk === "\x1Bf" || chunk === "\x1B[1;5C") {
|
|
2397
|
-
this.#input.moveWordRight();
|
|
2398
|
-
this.#syncInput();
|
|
2399
|
-
this.#queueRender();
|
|
2400
|
-
return;
|
|
2401
|
-
}
|
|
2402
|
-
if (chunk === "\x1B[3~") {
|
|
2403
|
-
this.#input.deleteForward();
|
|
2404
|
-
this.#syncInput();
|
|
2405
|
-
this.#queueRender();
|
|
2406
|
-
return;
|
|
2407
|
-
}
|
|
2408
|
-
if (chunk === "\x17") {
|
|
2409
|
-
this.#input.deleteWordLeft();
|
|
2410
|
-
this.#syncInput();
|
|
2411
|
-
this.#queueRender();
|
|
2412
|
-
return;
|
|
2413
|
-
}
|
|
2414
|
-
if (chunk === "\x15") {
|
|
2415
|
-
this.#input.clearBeforeCursor();
|
|
2416
|
-
this.#syncInput();
|
|
2417
|
-
this.#queueRender();
|
|
2418
|
-
return;
|
|
2419
|
-
}
|
|
2420
|
-
if (chunk === "\v") {
|
|
2421
|
-
this.#input.clearAfterCursor();
|
|
2422
|
-
this.#syncInput();
|
|
2423
|
-
this.#queueRender();
|
|
2424
|
-
return;
|
|
2425
|
-
}
|
|
2426
|
-
if (chunk === "\x1B[5~") {
|
|
2427
|
-
this.#scroll(8);
|
|
2428
|
-
return;
|
|
2429
|
-
}
|
|
2430
|
-
if (chunk === "\x1B[6~") {
|
|
2431
|
-
this.#scroll(-8);
|
|
2432
|
-
return;
|
|
2433
|
-
}
|
|
2434
|
-
if (this.#state.overlay === "confirm-delete") {
|
|
2435
|
-
const key = chunk.toLowerCase();
|
|
2436
|
-
if (CONFIRM_KEYS.has(key))
|
|
2437
|
-
this.#confirmTopicDelete();
|
|
2438
|
-
else if (CANCEL_KEYS.has(key) || chunk === "\x1B") {
|
|
2439
|
-
const cancelled = { ...this.#state, overlay: null, pendingDeleteTopicId: undefined };
|
|
2440
|
-
this.#state = this.#state.topicPickerRoot ? openTopicPicker(cancelled, undefined, true) : cancelled;
|
|
2441
|
-
this.#queueRender();
|
|
2442
|
-
}
|
|
2443
|
-
return;
|
|
2444
|
-
}
|
|
2445
|
-
if (this.#state.overlay === "background-session") {
|
|
2446
|
-
if (chunk === "\x1B") {
|
|
2447
|
-
this.#state = { ...this.#state, overlay: "topics" };
|
|
2448
|
-
this.#queueRender();
|
|
2449
|
-
}
|
|
2450
|
-
return;
|
|
2451
|
-
}
|
|
2452
|
-
if (this.#state.overlay === "topics") {
|
|
2453
|
-
this.#handleTopicPickerInput(chunk);
|
|
2454
|
-
return;
|
|
2455
|
-
}
|
|
2456
|
-
if (this.#state.overlay === "models") {
|
|
2457
|
-
if (chunk === "\x1B[A")
|
|
2458
|
-
this.#moveModelPicker(-1);
|
|
2459
|
-
else if (chunk === "\x1B[B")
|
|
2460
|
-
this.#moveModelPicker(1);
|
|
2461
|
-
else if (chunk === "\r")
|
|
2462
|
-
this.#selectPickedModel();
|
|
2463
|
-
else if (chunk === "\x1B") {
|
|
2464
|
-
this.#state = { ...this.#state, overlay: null };
|
|
2465
|
-
this.#queueRender();
|
|
2466
|
-
}
|
|
2467
|
-
return;
|
|
2468
|
-
}
|
|
2469
|
-
if (this.#state.overlay === "effort") {
|
|
2470
|
-
if (chunk === "\x1B[A")
|
|
2471
|
-
this.#moveEffortPicker(-1);
|
|
2472
|
-
else if (chunk === "\x1B[B")
|
|
2473
|
-
this.#moveEffortPicker(1);
|
|
2474
|
-
else if (chunk === "\r")
|
|
2475
|
-
this.#selectPickedEffort();
|
|
2476
|
-
else if (chunk === "\x1B") {
|
|
2477
|
-
this.#state = { ...this.#state, overlay: null };
|
|
2478
|
-
this.#queueRender();
|
|
2479
|
-
}
|
|
2480
|
-
return;
|
|
2481
|
-
}
|
|
2482
|
-
if (this.#state.overlay === "vault") {
|
|
2483
|
-
if (this.#state.vaultMode === "list" || this.#state.vaultMode === "confirm-delete") {
|
|
2484
|
-
this.#handleVaultListInput(chunk);
|
|
2485
|
-
return;
|
|
2486
|
-
}
|
|
2487
|
-
if (chunk === "\x1B") {
|
|
2488
|
-
this.#cancelVaultForm();
|
|
2489
|
-
return;
|
|
2490
|
-
}
|
|
2491
|
-
}
|
|
2492
|
-
if (chunk === "\x1B[A") {
|
|
2493
|
-
if (activeQuestion(this.#state))
|
|
2494
|
-
this.#moveAskChoice(-1);
|
|
2495
|
-
else if (this.#suggestionCount() > 0)
|
|
2496
|
-
this.#moveSuggestion(-1);
|
|
2497
|
-
else if (this.#input.isOnFirstLine) {
|
|
2498
|
-
const previous = this.#history.previous(this.#input.text);
|
|
2499
|
-
if (previous !== null)
|
|
2500
|
-
this.#replaceInput(previous);
|
|
2501
|
-
} else {
|
|
2502
|
-
this.#input.moveUp();
|
|
2503
|
-
this.#syncInput();
|
|
2504
|
-
this.#queueRender();
|
|
2505
|
-
}
|
|
2506
|
-
return;
|
|
2507
|
-
}
|
|
2508
|
-
if (chunk === "\x1B[B") {
|
|
2509
|
-
if (activeQuestion(this.#state))
|
|
2510
|
-
this.#moveAskChoice(1);
|
|
2511
|
-
else if (this.#suggestionCount() > 0)
|
|
2512
|
-
this.#moveSuggestion(1);
|
|
2513
|
-
else if (this.#input.isOnLastLine) {
|
|
2514
|
-
const next = this.#history.next();
|
|
2515
|
-
if (next !== null)
|
|
2516
|
-
this.#replaceInput(next);
|
|
2517
|
-
} else {
|
|
2518
|
-
this.#input.moveDown();
|
|
2519
|
-
this.#syncInput();
|
|
2520
|
-
this.#queueRender();
|
|
2521
|
-
}
|
|
2522
|
-
return;
|
|
2523
|
-
}
|
|
2524
|
-
if (chunk === "\t" || chunk === "\x1B[Z") {
|
|
2525
|
-
const completed = completeCommand(this.#input.text, this.#state.suggestionIndex);
|
|
2526
|
-
if (completed !== null)
|
|
2527
|
-
this.#replaceInput(completed);
|
|
2528
|
-
else
|
|
2529
|
-
this.#applyPathCompletion(true);
|
|
2530
|
-
return;
|
|
2531
|
-
}
|
|
2532
|
-
if (chunk === "\r") {
|
|
2533
|
-
if (this.#state.overlay !== "vault" && !activeQuestion(this.#state) && commandSuggestions(this.#input.text).length === 0 && this.#pathItems().length > 0) {
|
|
2534
|
-
this.#applyPathCompletion(false);
|
|
2535
|
-
return;
|
|
2536
|
-
}
|
|
2537
|
-
this.#submit();
|
|
2538
|
-
return;
|
|
2539
|
-
}
|
|
2540
|
-
if (chunk === "\x1B\r" || chunk === `\x1B
|
|
2541
|
-
`) {
|
|
2542
|
-
this.#input.insert(`
|
|
2543
|
-
`);
|
|
2544
|
-
this.#syncInput();
|
|
2545
|
-
this.#queueRender();
|
|
2546
|
-
return;
|
|
2547
|
-
}
|
|
2548
|
-
if (chunk === "\x7F" || chunk === "\b") {
|
|
2549
|
-
this.#input.backspace();
|
|
2550
|
-
this.#syncInput();
|
|
2551
|
-
this.#queueRender();
|
|
2552
|
-
return;
|
|
2553
|
-
}
|
|
2554
|
-
if (chunk === "\x1B") {
|
|
2555
|
-
if (escapeStopsActiveTurn(this.#state)) {
|
|
2556
|
-
this.#abort();
|
|
2557
|
-
return;
|
|
2558
|
-
}
|
|
2559
|
-
this.#input.setText("");
|
|
2560
|
-
this.#history.reset();
|
|
2561
|
-
this.#syncInput();
|
|
2562
|
-
const cancelled = { ...this.#state, overlay: null, creatingTopic: false };
|
|
2563
|
-
this.#state = this.#state.topicPickerRoot ? openTopicPicker(cancelled, undefined, true) : cancelled;
|
|
2564
|
-
this.#queueRender();
|
|
2565
|
-
return;
|
|
2566
|
-
}
|
|
2567
|
-
if (chunk.startsWith("\x1B"))
|
|
2568
|
-
return;
|
|
2569
|
-
const printable = [...chunk.replaceAll("\r", "").replaceAll(`
|
|
2570
|
-
`, "").replaceAll("\t", " ")].filter((character) => {
|
|
2571
|
-
const code = character.charCodeAt(0);
|
|
2572
|
-
return code > 31 && code !== 127;
|
|
2573
|
-
}).join("");
|
|
2574
|
-
if (printable) {
|
|
2575
|
-
this.#input.insert(printable);
|
|
2576
|
-
this.#syncInput();
|
|
2577
|
-
this.#queueRender();
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
async#submit() {
|
|
2581
|
-
if (this.#state.overlay === "vault" && this.#state.vaultMode !== "list" && this.#state.vaultMode !== "confirm-delete") {
|
|
2582
|
-
await this.#submitVaultField();
|
|
2583
|
-
return;
|
|
2584
|
-
}
|
|
2585
|
-
const ask = activeQuestion(this.#state);
|
|
2586
|
-
if (ask?.askUserQuestion?.choices.length) {
|
|
2587
|
-
const index = Math.min(this.#state.askChoiceIndex, ask.askUserQuestion.choices.length - 1);
|
|
2588
|
-
const choice = ask.askUserQuestion.choices[index];
|
|
2589
|
-
const result = await this.#client.answerQuestion(ask.topicId, ask.id, choice.label);
|
|
2590
|
-
this.#state = {
|
|
2591
|
-
...this.#state,
|
|
2592
|
-
askChoiceIndex: 0,
|
|
2593
|
-
notice: result.ok ? undefined : result.error
|
|
2594
|
-
};
|
|
2595
|
-
this.#queueRender();
|
|
2596
|
-
return;
|
|
2597
|
-
}
|
|
2598
|
-
const text = this.#input.text.trim();
|
|
2599
|
-
if (!text)
|
|
2600
|
-
return;
|
|
2601
|
-
if (this.#state.creatingTopic) {
|
|
2602
|
-
this.#input.setText("");
|
|
2603
|
-
this.#syncInput();
|
|
2604
|
-
this.#state = { ...this.#state, creatingTopic: false, notice: undefined };
|
|
2605
|
-
await this.#createTopic(text);
|
|
2606
|
-
return;
|
|
2607
|
-
}
|
|
2608
|
-
if (!isVaultCommandLine(text)) {
|
|
2609
|
-
this.#history.record(text);
|
|
2610
|
-
this.#client.appendInputHistory?.(text);
|
|
2611
|
-
}
|
|
2612
|
-
this.#input.setText("");
|
|
2613
|
-
this.#syncInput();
|
|
2614
|
-
this.#state = {
|
|
2615
|
-
...this.#state,
|
|
2616
|
-
overlay: null,
|
|
2617
|
-
notice: undefined
|
|
2618
|
-
};
|
|
2619
|
-
if (text.startsWith("/")) {
|
|
2620
|
-
await this.#runCommand(text);
|
|
2621
|
-
return;
|
|
2622
|
-
}
|
|
2623
|
-
const topic = activeTopic(this.#state);
|
|
2624
|
-
if (!topic) {
|
|
2625
|
-
this.#state = { ...this.#state, notice: "No topic selected" };
|
|
2626
|
-
this.#queueRender();
|
|
2627
|
-
return;
|
|
2628
|
-
}
|
|
2629
|
-
const optimisticQueryId = `terminal-pending-${Date.now()}`;
|
|
2630
|
-
this.#state = applyRuntimeEvent({ ...this.#state, scrollOffset: 0 }, {
|
|
2631
|
-
type: "ai-status",
|
|
2632
|
-
topicId: topic.id,
|
|
2633
|
-
payload: { kind: "ai_active", queryId: optimisticQueryId }
|
|
2634
|
-
});
|
|
2635
|
-
this.#queueRender();
|
|
2636
|
-
try {
|
|
2637
|
-
const message = await this.#client.sendMessage(topic, text);
|
|
2638
|
-
this.#state = upsertMessage(this.#state, message);
|
|
2639
|
-
} catch (error) {
|
|
2640
|
-
if (this.#state.activity[topic.id]?.queryId === optimisticQueryId) {
|
|
2641
|
-
this.#state = applyRuntimeEvent(this.#state, {
|
|
2642
|
-
type: "ai-status",
|
|
2643
|
-
topicId: topic.id,
|
|
2644
|
-
payload: {
|
|
2645
|
-
kind: "ai_error",
|
|
2646
|
-
queryId: optimisticQueryId,
|
|
2647
|
-
error: "Message could not be sent"
|
|
2648
|
-
}
|
|
2649
|
-
});
|
|
2650
|
-
}
|
|
2651
|
-
this.#state = {
|
|
2652
|
-
...this.#state,
|
|
2653
|
-
notice: error instanceof Error ? error.message : String(error)
|
|
2654
|
-
};
|
|
2655
|
-
}
|
|
2656
|
-
this.#queueRender();
|
|
2657
|
-
}
|
|
2658
|
-
async#runCommand(commandLine) {
|
|
2659
|
-
if (isVaultCommandLine(commandLine)) {
|
|
2660
|
-
if (commandLine.trim().toLowerCase() !== "/vault") {
|
|
2661
|
-
this.#state = { ...this.#state, notice: "Use /vault to open the Vault manager." };
|
|
2662
|
-
this.#queueRender();
|
|
2663
|
-
return;
|
|
2664
|
-
}
|
|
2665
|
-
await this.#openVault();
|
|
2666
|
-
return;
|
|
2667
|
-
}
|
|
2668
|
-
const [command = "", ...args] = commandLine.slice(1).trim().split(/\s+/);
|
|
2669
|
-
if (command === "quit" || command === "exit") {
|
|
2670
|
-
this.#requestExit();
|
|
2671
|
-
return;
|
|
2672
|
-
}
|
|
2673
|
-
if (command === "abort") {
|
|
2674
|
-
await this.#abort();
|
|
2675
|
-
return;
|
|
2676
|
-
}
|
|
2677
|
-
if (command === "help") {
|
|
2678
|
-
this.#state = { ...this.#state, overlay: "help" };
|
|
2679
|
-
this.#queueRender();
|
|
2680
|
-
return;
|
|
2681
|
-
}
|
|
2682
|
-
if (command === "status") {
|
|
2683
|
-
this.#state = { ...this.#state, overlay: "status" };
|
|
2684
|
-
this.#queueRender();
|
|
2685
|
-
return;
|
|
2686
|
-
}
|
|
2687
|
-
if (command === "model") {
|
|
2688
|
-
if (args.length > 0) {
|
|
2689
|
-
this.#state = { ...this.#state, notice: "Usage: /model" };
|
|
2690
|
-
this.#queueRender();
|
|
2691
|
-
return;
|
|
2692
|
-
}
|
|
2693
|
-
const topic = activeTopic(this.#state);
|
|
2694
|
-
if (!topic) {
|
|
2695
|
-
this.#state = { ...this.#state, notice: "No topic selected" };
|
|
2696
|
-
this.#queueRender();
|
|
2697
|
-
return;
|
|
2698
|
-
}
|
|
2699
|
-
const currentModel = topic.effectiveModel ?? topic.defaultModel;
|
|
2700
|
-
const currentIndex = SELECTABLE_MODELS.findIndex((candidate) => candidate.model === currentModel);
|
|
2701
|
-
this.#state = {
|
|
2702
|
-
...this.#state,
|
|
2703
|
-
overlay: "models",
|
|
2704
|
-
modelPickerIndex: Math.max(0, currentIndex)
|
|
2705
|
-
};
|
|
2706
|
-
this.#queueRender();
|
|
2707
|
-
return;
|
|
2708
|
-
}
|
|
2709
|
-
if (command === "effort") {
|
|
2710
|
-
if (args.length > 0) {
|
|
2711
|
-
this.#state = { ...this.#state, notice: "Usage: /effort" };
|
|
2712
|
-
this.#queueRender();
|
|
2713
|
-
return;
|
|
2714
|
-
}
|
|
2715
|
-
const topic = activeTopic(this.#state);
|
|
2716
|
-
if (!topic) {
|
|
2717
|
-
this.#state = { ...this.#state, notice: "No topic selected" };
|
|
2718
|
-
this.#queueRender();
|
|
2719
|
-
return;
|
|
2720
|
-
}
|
|
2721
|
-
const currentEffort = topic.effectiveEffort ?? topic.defaultEffort;
|
|
2722
|
-
const currentIndex = selectableEfforts(topic).indexOf(currentEffort);
|
|
2723
|
-
this.#state = {
|
|
2724
|
-
...this.#state,
|
|
2725
|
-
overlay: "effort",
|
|
2726
|
-
effortPickerIndex: Math.max(0, currentIndex)
|
|
2727
|
-
};
|
|
2728
|
-
this.#queueRender();
|
|
2729
|
-
return;
|
|
2730
|
-
}
|
|
2731
|
-
if (command === "compact") {
|
|
2732
|
-
const topic = activeTopic(this.#state);
|
|
2733
|
-
if (!topic) {
|
|
2734
|
-
this.#state = { ...this.#state, notice: "No topic selected" };
|
|
2735
|
-
this.#queueRender();
|
|
2736
|
-
return;
|
|
2737
|
-
}
|
|
2738
|
-
const queryId = `terminal-compact-${Date.now()}`;
|
|
2739
|
-
this.#state = applyRuntimeEvent({ ...this.#state, notice: "Compacting context\u2026" }, {
|
|
2740
|
-
type: "ai-status",
|
|
2741
|
-
topicId: topic.id,
|
|
2742
|
-
payload: { kind: "ai_active", queryId }
|
|
2743
|
-
});
|
|
2744
|
-
this.#queueRender();
|
|
2745
|
-
try {
|
|
2746
|
-
const notice = await this.#client.compactTopic(topic);
|
|
2747
|
-
this.#state = applyRuntimeEvent({ ...this.#state, notice }, {
|
|
2748
|
-
type: "ai-status",
|
|
2749
|
-
topicId: topic.id,
|
|
2750
|
-
payload: { kind: "ai_done", queryId }
|
|
2751
|
-
});
|
|
2752
|
-
} catch (error) {
|
|
2753
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2754
|
-
this.#state = applyRuntimeEvent({ ...this.#state, notice: message }, {
|
|
2755
|
-
type: "ai-status",
|
|
2756
|
-
topicId: topic.id,
|
|
2757
|
-
payload: { kind: "ai_error", queryId, error: message }
|
|
2758
|
-
});
|
|
2759
|
-
}
|
|
2760
|
-
this.#queueRender();
|
|
2761
|
-
return;
|
|
2762
|
-
}
|
|
2763
|
-
if (command === "topics") {
|
|
2764
|
-
this.#toggleTopics(true);
|
|
2765
|
-
return;
|
|
2766
|
-
}
|
|
2767
|
-
if (command === "new") {
|
|
2768
|
-
if (args.length > 0) {
|
|
2769
|
-
this.#state = { ...this.#state, notice: "Usage: /new" };
|
|
2770
|
-
this.#queueRender();
|
|
2771
|
-
return;
|
|
2772
|
-
}
|
|
2773
|
-
const topic = activeTopic(this.#state);
|
|
2774
|
-
if (!topic) {
|
|
2775
|
-
this.#state = { ...this.#state, notice: "No topic selected" };
|
|
2776
|
-
this.#queueRender();
|
|
2777
|
-
return;
|
|
2778
|
-
}
|
|
2779
|
-
try {
|
|
2780
|
-
const notice = await this.#client.resetTopic(topic);
|
|
2781
|
-
this.#state = { ...this.#state, notice };
|
|
2782
|
-
} catch (error) {
|
|
2783
|
-
this.#state = {
|
|
2784
|
-
...this.#state,
|
|
2785
|
-
notice: error instanceof Error ? error.message : String(error)
|
|
2786
|
-
};
|
|
2787
|
-
}
|
|
2788
|
-
this.#queueRender();
|
|
2789
|
-
return;
|
|
2790
|
-
}
|
|
2791
|
-
if (command === "fork" || command === "spawn") {
|
|
2792
|
-
const topic = activeTopic(this.#state);
|
|
2793
|
-
if (!topic) {
|
|
2794
|
-
this.#state = { ...this.#state, notice: "No topic selected" };
|
|
2795
|
-
this.#queueRender();
|
|
2796
|
-
return;
|
|
2797
|
-
}
|
|
2798
|
-
await this.#deriveTopic(topic, command === "fork", args.join(" ") || undefined);
|
|
2799
|
-
return;
|
|
2800
|
-
}
|
|
2801
|
-
if (command === "del") {
|
|
2802
|
-
if (args.length > 0) {
|
|
2803
|
-
this.#state = { ...this.#state, notice: "Usage: /del" };
|
|
2804
|
-
this.#queueRender();
|
|
2805
|
-
return;
|
|
2806
|
-
}
|
|
2807
|
-
const topic = activeTopic(this.#state);
|
|
2808
|
-
if (!topic) {
|
|
2809
|
-
this.#state = { ...this.#state, notice: "No topic selected" };
|
|
2810
|
-
this.#queueRender();
|
|
2811
|
-
return;
|
|
2812
|
-
}
|
|
2813
|
-
this.#requestTopicDelete(topic);
|
|
2814
|
-
return;
|
|
2815
|
-
}
|
|
2816
|
-
if (command === "copy") {
|
|
2817
|
-
this.#copy(args[0]?.toLowerCase() === "all");
|
|
2818
|
-
return;
|
|
2819
|
-
}
|
|
2820
|
-
this.#state = { ...this.#state, notice: `Unknown command: /${command}` };
|
|
2821
|
-
this.#queueRender();
|
|
2822
|
-
}
|
|
2823
|
-
async#activateTopic(topicId) {
|
|
2824
|
-
this.#state = selectTopic(this.#state, topicId);
|
|
2825
|
-
this.#queueRender();
|
|
2826
|
-
try {
|
|
2827
|
-
await this.#loadActiveMessages();
|
|
2828
|
-
} catch (error) {
|
|
2829
|
-
this.#state = {
|
|
2830
|
-
...this.#state,
|
|
2831
|
-
notice: error instanceof Error ? error.message : String(error)
|
|
2832
|
-
};
|
|
2833
|
-
}
|
|
2834
|
-
this.#queueRender();
|
|
2835
|
-
}
|
|
2836
|
-
#cycleTopic(delta) {
|
|
2837
|
-
if (this.#state.topics.length === 0)
|
|
2838
|
-
return;
|
|
2839
|
-
const index = this.#state.topics.findIndex((topic) => topic.id === this.#state.activeTopicId);
|
|
2840
|
-
const next = (Math.max(0, index) + delta + this.#state.topics.length) % this.#state.topics.length;
|
|
2841
|
-
this.#activateTopic(this.#state.topics[next].id);
|
|
2842
|
-
}
|
|
2843
|
-
#toggleTopics(forceOpen = false) {
|
|
2844
|
-
this.#state = forceOpen || this.#state.overlay !== "topics" ? openTopicPicker(this.#state) : { ...this.#state, overlay: null };
|
|
2845
|
-
this.#queueRender();
|
|
2846
|
-
}
|
|
2847
|
-
#handleTopicPickerInput(chunk) {
|
|
2848
|
-
const key = chunk.toLowerCase();
|
|
2849
|
-
if (chunk === "\x1B[A")
|
|
2850
|
-
this.#moveTopicPicker(-1);
|
|
2851
|
-
else if (chunk === "\x1B[B")
|
|
2852
|
-
this.#moveTopicPicker(1);
|
|
2853
|
-
else if (chunk === "\r")
|
|
2854
|
-
this.#selectPickedTopic();
|
|
2855
|
-
else if (NEW_TOPIC_KEYS.has(key)) {
|
|
2856
|
-
this.#openNewTopicComposer();
|
|
2857
|
-
} else if (DELETE_TOPIC_KEYS.has(key)) {
|
|
2858
|
-
const topic = pickedTopic(this.#state);
|
|
2859
|
-
if (topic)
|
|
2860
|
-
this.#requestTopicDelete(topic);
|
|
2861
|
-
else {
|
|
2862
|
-
this.#state = { ...this.#state, notice: "Background sessions are read-only" };
|
|
2863
|
-
this.#queueRender();
|
|
2864
|
-
}
|
|
2865
|
-
} else if (chunk === "\x1B") {
|
|
2866
|
-
if (this.#state.topicPickerRoot)
|
|
2867
|
-
this.#requestExit();
|
|
2868
|
-
else {
|
|
2869
|
-
this.#state = { ...this.#state, overlay: null };
|
|
2870
|
-
this.#queueRender();
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
}
|
|
2874
|
-
#openNewTopicComposer() {
|
|
2875
|
-
this.#state = startTopicCreation(this.#state);
|
|
2876
|
-
this.#replaceInput("");
|
|
2877
|
-
}
|
|
2878
|
-
async#createTopic(title) {
|
|
2879
|
-
const topicPickerRoot = this.#state.topicPickerRoot;
|
|
2880
|
-
try {
|
|
2881
|
-
const created = await this.#client.createTopic(title, this.#options.defaultAgent);
|
|
2882
|
-
this.#state = focusCreatedTopic(this.#state, created);
|
|
2883
|
-
await this.#refreshTopics(created.title);
|
|
2884
|
-
this.#state = selectTopic(this.#state, created.id);
|
|
2885
|
-
await this.#loadActiveMessages();
|
|
2886
|
-
this.#state = { ...this.#state, notice: `Created ${created.title}` };
|
|
2887
|
-
} catch (error) {
|
|
2888
|
-
const failed = {
|
|
2889
|
-
...this.#state,
|
|
2890
|
-
notice: error instanceof Error ? error.message : String(error)
|
|
2891
|
-
};
|
|
2892
|
-
this.#state = topicPickerRoot ? openTopicPicker(failed, failed.notice, true) : failed;
|
|
2893
|
-
}
|
|
2894
|
-
this.#queueRender();
|
|
2895
|
-
}
|
|
2896
|
-
async#deriveTopic(topic, copyHistory, name) {
|
|
2897
|
-
try {
|
|
2898
|
-
const derived = await this.#client.deriveTopic(topic, copyHistory, name);
|
|
2899
|
-
this.#state = focusCreatedTopic(this.#state, derived);
|
|
2900
|
-
await this.#refreshTopics(derived.title);
|
|
2901
|
-
this.#state = selectTopic(this.#state, derived.id);
|
|
2902
|
-
await this.#loadActiveMessages();
|
|
2903
|
-
this.#state = {
|
|
2904
|
-
...this.#state,
|
|
2905
|
-
notice: copyHistory ? `forked into "${derived.title}"` : `spawned "${derived.title}"`
|
|
2906
|
-
};
|
|
2907
|
-
} catch (error) {
|
|
2908
|
-
this.#state = {
|
|
2909
|
-
...this.#state,
|
|
2910
|
-
notice: error instanceof Error ? error.message : String(error)
|
|
2911
|
-
};
|
|
2912
|
-
}
|
|
2913
|
-
this.#queueRender();
|
|
2914
|
-
}
|
|
2915
|
-
#moveTopicPicker(delta) {
|
|
2916
|
-
this.#state = moveTopicPickerSelection(this.#state, delta);
|
|
2917
|
-
this.#queueRender();
|
|
2918
|
-
}
|
|
2919
|
-
#selectPickedTopic() {
|
|
2920
|
-
const background = pickedBackgroundSession(this.#state);
|
|
2921
|
-
if (background) {
|
|
2922
|
-
this.#state = { ...this.#state, overlay: "background-session", notice: undefined };
|
|
2923
|
-
this.#queueRender();
|
|
2924
|
-
return;
|
|
2925
|
-
}
|
|
2926
|
-
const topic = pickedTopic(this.#state);
|
|
2927
|
-
if (topic)
|
|
2928
|
-
this.#activateTopic(topic.id);
|
|
2929
|
-
}
|
|
2930
|
-
#moveModelPicker(delta) {
|
|
2931
|
-
const count = SELECTABLE_MODELS.length;
|
|
2932
|
-
if (count === 0)
|
|
2933
|
-
return;
|
|
2934
|
-
this.#state = {
|
|
2935
|
-
...this.#state,
|
|
2936
|
-
modelPickerIndex: (this.#state.modelPickerIndex + delta + count) % count
|
|
2937
|
-
};
|
|
2938
|
-
this.#queueRender();
|
|
2939
|
-
}
|
|
2940
|
-
async#openVault() {
|
|
2941
|
-
if (!this.#client.listVaultEntries || !this.#client.saveVaultEntry || !this.#client.deleteVaultEntry) {
|
|
2942
|
-
this.#state = { ...this.#state, notice: "Vault management is unavailable for this client." };
|
|
2943
|
-
this.#queueRender();
|
|
2944
|
-
return;
|
|
2945
|
-
}
|
|
2946
|
-
try {
|
|
2947
|
-
const entries = await this.#client.listVaultEntries();
|
|
2948
|
-
this.#vaultDraftValue = "";
|
|
2949
|
-
this.#state = {
|
|
2950
|
-
...this.#state,
|
|
2951
|
-
overlay: "vault",
|
|
2952
|
-
notice: undefined,
|
|
2953
|
-
vaultEntries: entries,
|
|
2954
|
-
vaultPickerIndex: Math.min(this.#state.vaultPickerIndex, Math.max(0, entries.length - 1)),
|
|
2955
|
-
vaultMode: "list",
|
|
2956
|
-
vaultDraftKey: undefined,
|
|
2957
|
-
vaultDraftDescription: "",
|
|
2958
|
-
vaultEditing: false,
|
|
2959
|
-
vaultNotice: undefined
|
|
2960
|
-
};
|
|
2961
|
-
} catch (error) {
|
|
2962
|
-
this.#state = {
|
|
2963
|
-
...this.#state,
|
|
2964
|
-
notice: error instanceof Error ? error.message : String(error)
|
|
2965
|
-
};
|
|
2966
|
-
}
|
|
2967
|
-
this.#queueRender();
|
|
2968
|
-
}
|
|
2969
|
-
#handleVaultListInput(chunk) {
|
|
2970
|
-
if (this.#state.vaultMode === "confirm-delete") {
|
|
2971
|
-
const key2 = chunk.toLowerCase();
|
|
2972
|
-
if (CONFIRM_KEYS.has(key2))
|
|
2973
|
-
this.#deleteSelectedVaultEntry();
|
|
2974
|
-
else if (CANCEL_KEYS.has(key2) || chunk === "\x1B") {
|
|
2975
|
-
this.#state = { ...this.#state, vaultMode: "list", vaultNotice: undefined };
|
|
2976
|
-
this.#queueRender();
|
|
2977
|
-
}
|
|
2978
|
-
return;
|
|
2979
|
-
}
|
|
2980
|
-
const key = chunk.toLowerCase();
|
|
2981
|
-
if (chunk === "\x1B[A")
|
|
2982
|
-
this.#moveVaultPicker(-1);
|
|
2983
|
-
else if (chunk === "\x1B[B")
|
|
2984
|
-
this.#moveVaultPicker(1);
|
|
2985
|
-
else if (key === "n" || key === "a")
|
|
2986
|
-
this.#beginVaultAdd();
|
|
2987
|
-
else if (chunk === "\r" || key === "e")
|
|
2988
|
-
this.#beginVaultEdit();
|
|
2989
|
-
else if (key === "d" || chunk === "\x1B[3~")
|
|
2990
|
-
this.#requestVaultDelete();
|
|
2991
|
-
else if (chunk === "\x1B") {
|
|
2992
|
-
this.#vaultDraftValue = "";
|
|
2993
|
-
this.#state = { ...this.#state, overlay: null, vaultNotice: undefined };
|
|
2994
|
-
this.#queueRender();
|
|
2995
|
-
}
|
|
2996
|
-
}
|
|
2997
|
-
#moveVaultPicker(delta) {
|
|
2998
|
-
const count = this.#state.vaultEntries.length;
|
|
2999
|
-
if (count === 0)
|
|
3000
|
-
return;
|
|
3001
|
-
this.#state = {
|
|
3002
|
-
...this.#state,
|
|
3003
|
-
vaultPickerIndex: (this.#state.vaultPickerIndex + delta + count) % count,
|
|
3004
|
-
vaultNotice: undefined
|
|
3005
|
-
};
|
|
3006
|
-
this.#queueRender();
|
|
3007
|
-
}
|
|
3008
|
-
#beginVaultAdd() {
|
|
3009
|
-
this.#vaultDraftValue = "";
|
|
3010
|
-
this.#state = {
|
|
3011
|
-
...this.#state,
|
|
3012
|
-
vaultMode: "key",
|
|
3013
|
-
vaultDraftKey: undefined,
|
|
3014
|
-
vaultDraftDescription: "",
|
|
3015
|
-
vaultEditing: false,
|
|
3016
|
-
vaultNotice: undefined
|
|
3017
|
-
};
|
|
3018
|
-
this.#replaceInput("");
|
|
3019
|
-
}
|
|
3020
|
-
#beginVaultEdit() {
|
|
3021
|
-
const selected = this.#state.vaultEntries[this.#state.vaultPickerIndex];
|
|
3022
|
-
if (!selected) {
|
|
3023
|
-
this.#beginVaultAdd();
|
|
3024
|
-
return;
|
|
3025
|
-
}
|
|
3026
|
-
this.#vaultDraftValue = "";
|
|
3027
|
-
this.#state = {
|
|
3028
|
-
...this.#state,
|
|
3029
|
-
vaultMode: "value",
|
|
3030
|
-
vaultDraftKey: selected.key,
|
|
3031
|
-
vaultDraftDescription: selected.description,
|
|
3032
|
-
vaultEditing: true,
|
|
3033
|
-
vaultNotice: undefined
|
|
3034
|
-
};
|
|
3035
|
-
this.#replaceInput("");
|
|
3036
|
-
}
|
|
3037
|
-
#requestVaultDelete() {
|
|
3038
|
-
if (!this.#state.vaultEntries[this.#state.vaultPickerIndex])
|
|
3039
|
-
return;
|
|
3040
|
-
this.#state = { ...this.#state, vaultMode: "confirm-delete", vaultNotice: undefined };
|
|
3041
|
-
this.#queueRender();
|
|
3042
|
-
}
|
|
3043
|
-
#cancelVaultForm() {
|
|
3044
|
-
this.#vaultDraftValue = "";
|
|
3045
|
-
this.#input.setText("");
|
|
3046
|
-
this.#syncInput();
|
|
3047
|
-
this.#state = {
|
|
3048
|
-
...this.#state,
|
|
3049
|
-
vaultMode: "list",
|
|
3050
|
-
vaultDraftKey: undefined,
|
|
3051
|
-
vaultDraftDescription: "",
|
|
3052
|
-
vaultEditing: false,
|
|
3053
|
-
vaultNotice: undefined
|
|
3054
|
-
};
|
|
3055
|
-
this.#queueRender();
|
|
3056
|
-
}
|
|
3057
|
-
async#submitVaultField() {
|
|
3058
|
-
const raw = this.#input.text;
|
|
3059
|
-
if (this.#state.vaultMode === "key") {
|
|
3060
|
-
const key2 = normalizeVaultKey(raw);
|
|
3061
|
-
if (!validateVaultKey(key2)) {
|
|
3062
|
-
this.#state = {
|
|
3063
|
-
...this.#state,
|
|
3064
|
-
vaultNotice: "Use A-Z, 0-9, and _. The key must start with a letter."
|
|
3065
|
-
};
|
|
3066
|
-
this.#queueRender();
|
|
3067
|
-
return;
|
|
3068
|
-
}
|
|
3069
|
-
if (this.#state.vaultEntries.some((entry) => entry.key === key2)) {
|
|
3070
|
-
this.#state = {
|
|
3071
|
-
...this.#state,
|
|
3072
|
-
vaultNotice: `${key2} already exists. Select it to update.`
|
|
3073
|
-
};
|
|
3074
|
-
this.#queueRender();
|
|
3075
|
-
return;
|
|
3076
|
-
}
|
|
3077
|
-
this.#state = {
|
|
3078
|
-
...this.#state,
|
|
3079
|
-
vaultMode: "value",
|
|
3080
|
-
vaultDraftKey: key2,
|
|
3081
|
-
vaultNotice: undefined
|
|
3082
|
-
};
|
|
3083
|
-
this.#replaceInput("");
|
|
3084
|
-
return;
|
|
3085
|
-
}
|
|
3086
|
-
if (this.#state.vaultMode === "value") {
|
|
3087
|
-
const valueBytes = Buffer.byteLength(raw, "utf8");
|
|
3088
|
-
if (valueBytes < VAULT_VALUE_MIN_BYTES || valueBytes > VAULT_VALUE_MAX_BYTES) {
|
|
3089
|
-
this.#state = {
|
|
3090
|
-
...this.#state,
|
|
3091
|
-
vaultNotice: `Secret must be ${VAULT_VALUE_MIN_BYTES}-${VAULT_VALUE_MAX_BYTES} bytes.`
|
|
3092
|
-
};
|
|
3093
|
-
this.#queueRender();
|
|
3094
|
-
return;
|
|
3095
|
-
}
|
|
3096
|
-
this.#vaultDraftValue = raw;
|
|
3097
|
-
this.#state = { ...this.#state, vaultMode: "description", vaultNotice: undefined };
|
|
3098
|
-
this.#replaceInput(this.#state.vaultDraftDescription);
|
|
3099
|
-
return;
|
|
3100
|
-
}
|
|
3101
|
-
if (raw.length > VAULT_DESCRIPTION_MAX_LENGTH) {
|
|
3102
|
-
this.#state = {
|
|
3103
|
-
...this.#state,
|
|
3104
|
-
vaultNotice: `Description must not exceed ${VAULT_DESCRIPTION_MAX_LENGTH} characters.`
|
|
3105
|
-
};
|
|
3106
|
-
this.#queueRender();
|
|
3107
|
-
return;
|
|
3108
|
-
}
|
|
3109
|
-
const key = this.#state.vaultDraftKey;
|
|
3110
|
-
if (!key || !this.#client.saveVaultEntry)
|
|
3111
|
-
return;
|
|
3112
|
-
this.#input.setText("");
|
|
3113
|
-
this.#syncInput();
|
|
3114
|
-
try {
|
|
3115
|
-
const result = await this.#client.saveVaultEntry(key, this.#vaultDraftValue, raw);
|
|
3116
|
-
const entries = this.#client.listVaultEntries ? await this.#client.listVaultEntries() : [];
|
|
3117
|
-
const selectedIndex = Math.max(0, entries.findIndex((entry) => entry.key === result.key));
|
|
3118
|
-
this.#state = {
|
|
3119
|
-
...this.#state,
|
|
3120
|
-
vaultEntries: entries,
|
|
3121
|
-
vaultPickerIndex: selectedIndex,
|
|
3122
|
-
vaultMode: "list",
|
|
3123
|
-
vaultDraftKey: undefined,
|
|
3124
|
-
vaultDraftDescription: "",
|
|
3125
|
-
vaultEditing: false,
|
|
3126
|
-
vaultNotice: `${result.updated ? "Updated" : "Added"} ${result.key}.`
|
|
3127
|
-
};
|
|
3128
|
-
} catch (error) {
|
|
3129
|
-
this.#state = {
|
|
3130
|
-
...this.#state,
|
|
3131
|
-
vaultMode: "list",
|
|
3132
|
-
vaultNotice: error instanceof Error ? error.message : String(error)
|
|
3133
|
-
};
|
|
3134
|
-
} finally {
|
|
3135
|
-
this.#vaultDraftValue = "";
|
|
3136
|
-
}
|
|
3137
|
-
this.#queueRender();
|
|
3138
|
-
}
|
|
3139
|
-
async#deleteSelectedVaultEntry() {
|
|
3140
|
-
const selected = this.#state.vaultEntries[this.#state.vaultPickerIndex];
|
|
3141
|
-
if (!selected || !this.#client.deleteVaultEntry)
|
|
3142
|
-
return;
|
|
3143
|
-
try {
|
|
3144
|
-
const deleted = await this.#client.deleteVaultEntry(selected.key);
|
|
3145
|
-
const entries = this.#client.listVaultEntries ? await this.#client.listVaultEntries() : [];
|
|
3146
|
-
this.#state = {
|
|
3147
|
-
...this.#state,
|
|
3148
|
-
vaultEntries: entries,
|
|
3149
|
-
vaultPickerIndex: Math.min(this.#state.vaultPickerIndex, Math.max(0, entries.length - 1)),
|
|
3150
|
-
vaultMode: "list",
|
|
3151
|
-
vaultNotice: deleted ? `Deleted ${selected.key}.` : `${selected.key} no longer exists.`
|
|
3152
|
-
};
|
|
3153
|
-
} catch (error) {
|
|
3154
|
-
this.#state = {
|
|
3155
|
-
...this.#state,
|
|
3156
|
-
vaultMode: "list",
|
|
3157
|
-
vaultNotice: error instanceof Error ? error.message : String(error)
|
|
3158
|
-
};
|
|
3159
|
-
}
|
|
3160
|
-
this.#queueRender();
|
|
3161
|
-
}
|
|
3162
|
-
async#selectPickedModel() {
|
|
3163
|
-
const topic = activeTopic(this.#state);
|
|
3164
|
-
const selected = SELECTABLE_MODELS[this.#state.modelPickerIndex];
|
|
3165
|
-
if (!topic || !selected)
|
|
3166
|
-
return;
|
|
3167
|
-
this.#state = { ...this.#state, overlay: null, notice: `Switching to ${selected.model}\u2026` };
|
|
3168
|
-
this.#queueRender();
|
|
3169
|
-
try {
|
|
3170
|
-
const notice = await this.#client.setModel(topic, selected.model);
|
|
3171
|
-
await this.#refreshTopics(topic.title);
|
|
3172
|
-
this.#state = { ...this.#state, notice };
|
|
3173
|
-
} catch (error) {
|
|
3174
|
-
this.#state = {
|
|
3175
|
-
...this.#state,
|
|
3176
|
-
notice: error instanceof Error ? error.message : String(error)
|
|
3177
|
-
};
|
|
3178
|
-
}
|
|
3179
|
-
this.#queueRender();
|
|
3180
|
-
}
|
|
3181
|
-
#moveEffortPicker(delta) {
|
|
3182
|
-
const count = selectableEfforts(activeTopic(this.#state)).length;
|
|
3183
|
-
this.#state = {
|
|
3184
|
-
...this.#state,
|
|
3185
|
-
effortPickerIndex: (this.#state.effortPickerIndex + delta + count) % count
|
|
3186
|
-
};
|
|
3187
|
-
this.#queueRender();
|
|
3188
|
-
}
|
|
3189
|
-
async#selectPickedEffort() {
|
|
3190
|
-
const topic = activeTopic(this.#state);
|
|
3191
|
-
const effort = selectableEfforts(topic)[this.#state.effortPickerIndex];
|
|
3192
|
-
if (!topic || !effort)
|
|
3193
|
-
return;
|
|
3194
|
-
this.#state = { ...this.#state, overlay: null, notice: `Setting effort to ${effort}\u2026` };
|
|
3195
|
-
this.#queueRender();
|
|
3196
|
-
try {
|
|
3197
|
-
const notice = await this.#client.setEffort(topic, effort);
|
|
3198
|
-
await this.#refreshTopics(topic.title);
|
|
3199
|
-
this.#state = { ...this.#state, notice };
|
|
3200
|
-
} catch (error) {
|
|
3201
|
-
this.#state = {
|
|
3202
|
-
...this.#state,
|
|
3203
|
-
notice: error instanceof Error ? error.message : String(error)
|
|
3204
|
-
};
|
|
3205
|
-
}
|
|
3206
|
-
this.#queueRender();
|
|
3207
|
-
}
|
|
3208
|
-
#pathItems() {
|
|
3209
|
-
const cursor = this.#input.cursor;
|
|
3210
|
-
const lineText = this.#input.text.split(`
|
|
3211
|
-
`)[cursor.row] ?? "";
|
|
3212
|
-
return pathSuggestions(lineText, cursor.col)?.items ?? [];
|
|
3213
|
-
}
|
|
3214
|
-
#suggestionCount() {
|
|
3215
|
-
const commands = commandSuggestions(this.#input.text).length;
|
|
3216
|
-
return commands > 0 ? commands : this.#pathItems().length;
|
|
3217
|
-
}
|
|
3218
|
-
#applyPathCompletion(keepTrigger) {
|
|
3219
|
-
const items = this.#pathItems();
|
|
3220
|
-
if (items.length === 0)
|
|
3221
|
-
return;
|
|
3222
|
-
const cursor = this.#input.cursor;
|
|
3223
|
-
const lines = this.#input.text.split(`
|
|
3224
|
-
`);
|
|
3225
|
-
const lineText = lines[cursor.row] ?? "";
|
|
3226
|
-
const selected = items[(this.#state.suggestionIndex + items.length) % items.length];
|
|
3227
|
-
const result = completePathToken(lineText, cursor.col, selected, { keepTrigger });
|
|
3228
|
-
if (!result)
|
|
3229
|
-
return;
|
|
3230
|
-
lines[cursor.row] = result.line;
|
|
3231
|
-
this.#input.setText(lines.join(`
|
|
3232
|
-
`), { row: cursor.row, col: result.col });
|
|
3233
|
-
this.#syncInput();
|
|
3234
|
-
this.#queueRender();
|
|
3235
|
-
}
|
|
3236
|
-
#moveSuggestion(delta) {
|
|
3237
|
-
const count = this.#suggestionCount();
|
|
3238
|
-
if (count === 0)
|
|
3239
|
-
return;
|
|
3240
|
-
this.#state = {
|
|
3241
|
-
...this.#state,
|
|
3242
|
-
suggestionIndex: (this.#state.suggestionIndex + delta + count) % count
|
|
3243
|
-
};
|
|
3244
|
-
this.#queueRender();
|
|
3245
|
-
}
|
|
3246
|
-
#requestTopicDelete(topic) {
|
|
3247
|
-
if (!topic)
|
|
3248
|
-
return;
|
|
3249
|
-
if (topic.kind === "manager") {
|
|
3250
|
-
const notice = "Manager topics cannot be deleted";
|
|
3251
|
-
this.#state = this.#state.topicPickerRoot ? openTopicPicker(this.#state, notice, true) : { ...this.#state, overlay: null, notice };
|
|
3252
|
-
this.#queueRender();
|
|
3253
|
-
return;
|
|
3254
|
-
}
|
|
3255
|
-
this.#state = {
|
|
3256
|
-
...this.#state,
|
|
3257
|
-
overlay: "confirm-delete",
|
|
3258
|
-
pendingDeleteTopicId: topic.id
|
|
3259
|
-
};
|
|
3260
|
-
this.#queueRender();
|
|
3261
|
-
}
|
|
3262
|
-
async#confirmTopicDelete() {
|
|
3263
|
-
const topicPickerRoot = this.#state.topicPickerRoot;
|
|
3264
|
-
const topic = this.#state.topics.find((candidate) => candidate.id === this.#state.pendingDeleteTopicId);
|
|
3265
|
-
if (!topic) {
|
|
3266
|
-
const missing = { ...this.#state, overlay: null, pendingDeleteTopicId: undefined };
|
|
3267
|
-
this.#state = topicPickerRoot ? openTopicPicker(missing, undefined, true) : missing;
|
|
3268
|
-
this.#queueRender();
|
|
3269
|
-
return;
|
|
3270
|
-
}
|
|
3271
|
-
this.#state = {
|
|
3272
|
-
...this.#state,
|
|
3273
|
-
overlay: null,
|
|
3274
|
-
pendingDeleteTopicId: undefined,
|
|
3275
|
-
notice: `Deleting ${topic.title}\u2026`
|
|
3276
|
-
};
|
|
3277
|
-
this.#queueRender();
|
|
3278
|
-
try {
|
|
3279
|
-
await this.#client.deleteTopic(topic);
|
|
3280
|
-
await this.#refreshTopics();
|
|
3281
|
-
this.#state = openTopicPicker(this.#state, `Deleted ${topic.title}`, topicPickerRoot);
|
|
3282
|
-
if (!topicPickerRoot)
|
|
3283
|
-
await this.#loadActiveMessages();
|
|
3284
|
-
} catch (error) {
|
|
3285
|
-
const failed = {
|
|
3286
|
-
...this.#state,
|
|
3287
|
-
notice: error instanceof Error ? error.message : String(error)
|
|
3288
|
-
};
|
|
3289
|
-
this.#state = topicPickerRoot ? openTopicPicker(failed, failed.notice, true) : failed;
|
|
3290
|
-
}
|
|
3291
|
-
this.#queueRender();
|
|
3292
|
-
}
|
|
3293
|
-
async#copy(all) {
|
|
3294
|
-
const text = all ? plainTranscript(this.#state) : activeMessages(this.#state).slice().reverse().find((message) => message.authorId === "ai" && message.text.trim())?.text;
|
|
3295
|
-
if (!text) {
|
|
3296
|
-
this.#state = {
|
|
3297
|
-
...this.#state,
|
|
3298
|
-
notice: all ? "Nothing to copy" : "No agent response to copy"
|
|
3299
|
-
};
|
|
3300
|
-
this.#queueRender();
|
|
3301
|
-
return;
|
|
3302
|
-
}
|
|
3303
|
-
try {
|
|
3304
|
-
const result = await copyToClipboard(text);
|
|
3305
|
-
this.#state = {
|
|
3306
|
-
...this.#state,
|
|
3307
|
-
notice: `${all ? "Transcript" : "Last response"} copied via ${result.method}${result.truncated ? " (truncated)" : ""}`
|
|
3308
|
-
};
|
|
3309
|
-
} catch (error) {
|
|
3310
|
-
this.#state = {
|
|
3311
|
-
...this.#state,
|
|
3312
|
-
notice: `Copy failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3313
|
-
};
|
|
3314
|
-
}
|
|
3315
|
-
this.#queueRender();
|
|
3316
|
-
}
|
|
3317
|
-
#handleMouseSelection(event) {
|
|
3318
|
-
if ((event.button & 3) !== 0 && event.kind !== "release")
|
|
3319
|
-
return;
|
|
3320
|
-
const point = { x: event.x, y: event.y };
|
|
3321
|
-
if (event.kind === "press") {
|
|
3322
|
-
this.#selection = { anchor: point, focus: point };
|
|
3323
|
-
this.#queueRender();
|
|
3324
|
-
return;
|
|
3325
|
-
}
|
|
3326
|
-
if (!this.#selection)
|
|
3327
|
-
return;
|
|
3328
|
-
this.#selection = { ...this.#selection, focus: point };
|
|
3329
|
-
if (event.kind === "drag") {
|
|
3330
|
-
this.#queueRender();
|
|
3331
|
-
return;
|
|
3332
|
-
}
|
|
3333
|
-
const selection = this.#selection;
|
|
3334
|
-
if (selection.anchor.x === selection.focus.x && selection.anchor.y === selection.focus.y) {
|
|
3335
|
-
this.#selection = null;
|
|
3336
|
-
this.#queueRender();
|
|
3337
|
-
return;
|
|
3338
|
-
}
|
|
3339
|
-
const text = screenSelectionText(this.#plainFrameLines, selection);
|
|
3340
|
-
if (!text) {
|
|
3341
|
-
this.#selection = null;
|
|
3342
|
-
this.#queueRender();
|
|
3343
|
-
return;
|
|
3344
|
-
}
|
|
3345
|
-
this.#copySelection(text);
|
|
3346
|
-
this.#queueRender();
|
|
3347
|
-
}
|
|
3348
|
-
async#copySelection(text) {
|
|
3349
|
-
try {
|
|
3350
|
-
const result = await copyToClipboard(text);
|
|
3351
|
-
this.#state = {
|
|
3352
|
-
...this.#state,
|
|
3353
|
-
notice: `Selection copied via ${result.method}${result.truncated ? " (truncated)" : ""}`
|
|
3354
|
-
};
|
|
3355
|
-
} catch (error) {
|
|
3356
|
-
this.#state = {
|
|
3357
|
-
...this.#state,
|
|
3358
|
-
notice: `Copy failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3359
|
-
};
|
|
3360
|
-
}
|
|
3361
|
-
this.#queueRender();
|
|
3362
|
-
}
|
|
3363
|
-
#moveAskChoice(delta) {
|
|
3364
|
-
const choices = activeQuestion(this.#state)?.askUserQuestion?.choices;
|
|
3365
|
-
if (!choices?.length)
|
|
3366
|
-
return;
|
|
3367
|
-
const next = (this.#state.askChoiceIndex + delta + choices.length) % choices.length;
|
|
3368
|
-
this.#state = { ...this.#state, askChoiceIndex: next };
|
|
3369
|
-
this.#queueRender();
|
|
3370
|
-
}
|
|
3371
|
-
#loadOlderHistory() {
|
|
3372
|
-
const topic = activeTopic(this.#state);
|
|
3373
|
-
if (!topic || !this.#client.listMessagePage)
|
|
3374
|
-
return;
|
|
3375
|
-
const history = this.#messageHistory.get(topic.id);
|
|
3376
|
-
if (!history || history.loading)
|
|
3377
|
-
return;
|
|
3378
|
-
if (!history.hasMore || !history.cursor) {
|
|
3379
|
-
this.#state = { ...this.#state, notice: "Start of conversation" };
|
|
3380
|
-
this.#queueRender();
|
|
3381
|
-
return;
|
|
3382
|
-
}
|
|
3383
|
-
const currentMax = maxConversationScrollOffset(this.#state, process.stdout.columns ?? 100, process.stdout.rows ?? 30);
|
|
3384
|
-
this.#state = { ...this.#state, scrollOffset: currentMax };
|
|
3385
|
-
this.#loadOlderMessages(topic.id, currentMax + 8);
|
|
3386
|
-
}
|
|
3387
|
-
#scroll(delta) {
|
|
3388
|
-
const maxOffset = maxConversationScrollOffset(this.#state, process.stdout.columns ?? 100, process.stdout.rows ?? 30);
|
|
3389
|
-
const desiredOffset = this.#state.scrollOffset + delta;
|
|
3390
|
-
this.#state = {
|
|
3391
|
-
...this.#state,
|
|
3392
|
-
scrollOffset: Math.min(maxOffset, Math.max(0, desiredOffset))
|
|
3393
|
-
};
|
|
3394
|
-
this.#queueRender();
|
|
3395
|
-
}
|
|
3396
|
-
async#abort() {
|
|
3397
|
-
const topic = activeTopic(this.#state);
|
|
3398
|
-
let aborted = false;
|
|
3399
|
-
try {
|
|
3400
|
-
aborted = topic ? await this.#client.abort(topic.id) : false;
|
|
3401
|
-
} catch (error) {
|
|
3402
|
-
this.#state = {
|
|
3403
|
-
...this.#state,
|
|
3404
|
-
notice: error instanceof Error ? error.message : String(error)
|
|
3405
|
-
};
|
|
3406
|
-
this.#queueRender();
|
|
3407
|
-
return;
|
|
3408
|
-
}
|
|
3409
|
-
this.#state = {
|
|
3410
|
-
...this.#state,
|
|
3411
|
-
notice: aborted ? "Turn aborted" : "Nothing is running"
|
|
3412
|
-
};
|
|
3413
|
-
this.#queueRender();
|
|
3414
|
-
}
|
|
3415
|
-
#handleInterrupt() {
|
|
3416
|
-
if (ctrlCExitsTopicPicker(this.#state)) {
|
|
3417
|
-
this.#requestExit();
|
|
3418
|
-
return;
|
|
3419
|
-
}
|
|
3420
|
-
if (this.#state.topicPickerRoot && this.#state.creatingTopic) {
|
|
3421
|
-
this.#lastInterruptAt = 0;
|
|
3422
|
-
this.#input.setText("");
|
|
3423
|
-
this.#history.reset();
|
|
3424
|
-
this.#syncInput();
|
|
3425
|
-
this.#state = openTopicPicker(this.#state, undefined, true);
|
|
3426
|
-
this.#queueRender();
|
|
3427
|
-
return;
|
|
3428
|
-
}
|
|
3429
|
-
const topic = activeTopic(this.#state);
|
|
3430
|
-
if (topic && this.#state.activity[topic.id]?.running) {
|
|
3431
|
-
this.#lastInterruptAt = 0;
|
|
3432
|
-
this.#abort();
|
|
3433
|
-
return;
|
|
3434
|
-
}
|
|
3435
|
-
if (this.#input.text || this.#state.overlay || this.#state.creatingTopic) {
|
|
3436
|
-
this.#lastInterruptAt = 0;
|
|
3437
|
-
if (this.#state.overlay === "vault")
|
|
3438
|
-
this.#vaultDraftValue = "";
|
|
3439
|
-
this.#input.setText("");
|
|
3440
|
-
this.#history.reset();
|
|
3441
|
-
this.#syncInput();
|
|
3442
|
-
this.#state = {
|
|
3443
|
-
...this.#state,
|
|
3444
|
-
overlay: null,
|
|
3445
|
-
pendingDeleteTopicId: undefined,
|
|
3446
|
-
creatingTopic: false,
|
|
3447
|
-
notice: "Input cleared"
|
|
3448
|
-
};
|
|
3449
|
-
this.#queueRender();
|
|
3450
|
-
return;
|
|
3451
|
-
}
|
|
3452
|
-
const now = Date.now();
|
|
3453
|
-
if (now - this.#lastInterruptAt <= 1500) {
|
|
3454
|
-
this.#requestExit();
|
|
3455
|
-
return;
|
|
3456
|
-
}
|
|
3457
|
-
this.#lastInterruptAt = now;
|
|
3458
|
-
this.#state = { ...this.#state, notice: "Press Ctrl-C again to exit" };
|
|
3459
|
-
this.#queueRender();
|
|
3460
|
-
}
|
|
3461
|
-
#requestExit() {
|
|
3462
|
-
if (!this.#running)
|
|
3463
|
-
return;
|
|
3464
|
-
this.#running = false;
|
|
3465
|
-
this.#finishRun?.();
|
|
3466
|
-
this.#finishRun = null;
|
|
3467
|
-
}
|
|
3468
|
-
async#cleanup(clientStartAttempted, uiActive) {
|
|
3469
|
-
if (this.#renderTimer)
|
|
3470
|
-
clearTimeout(this.#renderTimer);
|
|
3471
|
-
if (this.#animationTimer)
|
|
3472
|
-
clearInterval(this.#animationTimer);
|
|
3473
|
-
if (this.#backgroundRefreshTimer)
|
|
3474
|
-
clearInterval(this.#backgroundRefreshTimer);
|
|
3475
|
-
this.#renderTimer = undefined;
|
|
3476
|
-
this.#animationTimer = undefined;
|
|
3477
|
-
this.#backgroundRefreshTimer = undefined;
|
|
3478
|
-
this.#renderQueued = false;
|
|
3479
|
-
this.#screen.reset();
|
|
3480
|
-
if (uiActive) {
|
|
3481
|
-
process.stdin.off("data", this.#onData);
|
|
3482
|
-
process.stdout.off("resize", this.#onResize);
|
|
3483
|
-
process.off("SIGINT", this.#onSignal);
|
|
3484
|
-
process.off("SIGTERM", this.#onSignal);
|
|
3485
|
-
if (process.stdin.isTTY)
|
|
3486
|
-
process.stdin.setRawMode(false);
|
|
3487
|
-
process.stdin.pause();
|
|
3488
|
-
process.stdout.write(EXIT_ALT_SCREEN);
|
|
3489
|
-
}
|
|
3490
|
-
if (clientStartAttempted)
|
|
3491
|
-
await this.#client.stop();
|
|
3492
|
-
}
|
|
3493
|
-
}
|
|
3494
|
-
|
|
3495
|
-
// ../../adapters/terminal/src/index.ts
|
|
3496
|
-
function startTerminalAdapter(options) {
|
|
3497
|
-
const client = options.client ?? new EmbeddedNegotiumClient({
|
|
3498
|
-
userId: options.userId,
|
|
3499
|
-
port: options.port,
|
|
3500
|
-
startNode: options.startNode
|
|
3501
|
-
});
|
|
3502
|
-
const app = new TerminalApp(client, options);
|
|
3503
|
-
const completed = app.run();
|
|
3504
|
-
return {
|
|
3505
|
-
name: "terminal",
|
|
3506
|
-
completed,
|
|
3507
|
-
async stop() {
|
|
3508
|
-
app.stop();
|
|
3509
|
-
await completed;
|
|
3510
|
-
}
|
|
3511
|
-
};
|
|
3512
|
-
}
|
|
3513
|
-
var terminalAdapter = defineNegotiumAdapter({
|
|
3514
|
-
name: "terminal",
|
|
3515
|
-
capabilities: {
|
|
3516
|
-
localUserInput: true,
|
|
3517
|
-
topicManagement: true,
|
|
3518
|
-
externalPlacedTurn: false
|
|
3519
|
-
},
|
|
3520
|
-
projection: {
|
|
3521
|
-
transcript: "full",
|
|
3522
|
-
historyBackfill: true,
|
|
3523
|
-
externalAuthors: "native"
|
|
3524
|
-
},
|
|
3525
|
-
start: startTerminalAdapter
|
|
3526
|
-
});
|
|
3527
|
-
export {
|
|
3528
|
-
terminalAdapter,
|
|
3529
|
-
startTerminalAdapter,
|
|
3530
|
-
renderApp,
|
|
3531
|
-
TerminalApp,
|
|
3532
|
-
RemoteNegotiumClient,
|
|
3533
|
-
EmbeddedNegotiumClient
|
|
3534
|
-
};
|
|
3535
|
-
|
|
3536
|
-
//# debugId=2E8352AA30C2ACBA64756E2164756E21
|