dsh-connect 0.7.0 → 0.7.2
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/lib/index.d.ts +10 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +5 -2
- package/lib/index.js.map +1 -1
- package/lib/menu-controller.d.ts +71 -0
- package/lib/menu-controller.d.ts.map +1 -0
- package/lib/menu-controller.js +292 -0
- package/lib/menu-controller.js.map +1 -0
- package/lib/menus.d.ts +73 -0
- package/lib/menus.d.ts.map +1 -0
- package/lib/menus.js +116 -0
- package/lib/menus.js.map +1 -0
- package/lib/runner.d.ts +51 -89
- package/lib/runner.d.ts.map +1 -1
- package/lib/runner.js +63 -617
- package/lib/runner.js.map +1 -1
- package/lib/stream.d.ts +62 -2
- package/lib/stream.d.ts.map +1 -1
- package/lib/stream.js +270 -0
- package/lib/stream.js.map +1 -1
- package/lib/types.d.ts +11 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +2 -2
package/lib/runner.js
CHANGED
|
@@ -10,8 +10,10 @@ import { copyFile, readFile } from "node:fs/promises";
|
|
|
10
10
|
import { basename, isAbsolute, join } from "node:path";
|
|
11
11
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
12
12
|
import { createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
13
|
-
import { createAsyncQueue } from "./stream.js";
|
|
13
|
+
import { createAsyncQueue, applyStreamChunk, applyToolCall, toolCallSummary, questionTextOf, summarizeTurn, classifyError, mapReason, truncate, fmtTokens, textOf, } from "./stream.js";
|
|
14
14
|
import { helpText, parseCommand } from "./commands.js";
|
|
15
|
+
import { menuTitle, rootMenuSections, reasonLabel, goalPhaseLabel, listWorkspaces, PROGRESS_PRESET_MS } from "./menus.js";
|
|
16
|
+
import { MenuController } from "./menu-controller.js";
|
|
15
17
|
import { messages } from "./i18n.js";
|
|
16
18
|
import { DEFAULT_LOCK_TIMEOUT_MS, acquire as lockAcquire, canWrite as lockCanWrite, isLockTimedOut as lockIsTimedOut, release as lockRelease, } from "./mirror-lock.js";
|
|
17
19
|
import { formatRemindAt, parseRemindTime } from "./scheduler.js";
|
|
@@ -27,280 +29,10 @@ export function resolveConnectConfig(config) {
|
|
|
27
29
|
stateDir: config.stateDir,
|
|
28
30
|
autoMirror: config.autoMirror ?? true, // Enabled by default
|
|
29
31
|
streamHeartbeatMs: config.streamHeartbeatMs ?? 60_000,
|
|
30
|
-
notifyLevel: config.notifyLevel ?? "
|
|
32
|
+
notifyLevel: config.notifyLevel ?? "result",
|
|
31
33
|
progressTimeoutMs: config.progressTimeoutMs ?? 5 * 60_000, // Proactive progress notice after 5 min of silence
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Assemble one `assistant/chunk` event into the streaming queue with visible
|
|
36
|
-
* structure. DSH streams one block per LLM content block and block boundaries
|
|
37
|
-
* carry no newline of their own, so without separators every block (and the
|
|
38
|
-
* reasoning phase vs the final answer) would be glued together in the chat
|
|
39
|
-
* card. This inserts a blank line between blocks and before the answer, keeps
|
|
40
|
-
* the thinking hint once per turn, expands reasoning soft breaks (Feishu's
|
|
41
|
-
* streaming card collapses single `\n`), and falls back to whole-block text
|
|
42
|
-
* for blocks that arrived only via `block-end`. Every push refreshes
|
|
43
|
-
* `lastPushAt` so the liveness heartbeat knows the card is still moving.
|
|
44
|
-
* @param state - the turn's mutable assembly state.
|
|
45
|
-
* @param thinkingHint - localized hint text pushed at the first reasoning delta.
|
|
46
|
-
* @param chunk - the assistant chunk event's `chunk` payload.
|
|
47
|
-
*/
|
|
48
|
-
/** Every line-break run becomes a paragraph break (Feishu's streaming card collapses single `\n`). */
|
|
49
|
-
function expandLineBreaks(text) {
|
|
50
|
-
return text.replace(/\r\n?/g, "\n").replace(/\n+/g, "\n\n");
|
|
51
|
-
}
|
|
52
|
-
export function applyStreamChunk(state, thinkingHint, chunk, level = "full") {
|
|
53
|
-
if (chunk.type === "text-delta" || chunk.type === "reasoning-delta") {
|
|
54
|
-
const isReasoning = chunk.type === "reasoning-delta";
|
|
55
|
-
// Reasoning content is only streamed in `full` mode; `important` shows
|
|
56
|
-
// just the thinking hint and `result` shows nothing until the answer.
|
|
57
|
-
if (isReasoning && level !== "full") {
|
|
58
|
-
if (level === "important" && !state.hintPushed) {
|
|
59
|
-
state.hintPushed = true;
|
|
60
|
-
state.chunks.push(thinkingHint);
|
|
61
|
-
state.pushedAny = true;
|
|
62
|
-
state.lastPushAt = Date.now();
|
|
63
|
-
}
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (state.pushedAny) {
|
|
67
|
-
const newBlock = chunk.index !== undefined && state.lastIndex !== undefined && chunk.index !== state.lastIndex;
|
|
68
|
-
const reasoningToAnswer = !isReasoning && state.reasoning;
|
|
69
|
-
if (newBlock || reasoningToAnswer)
|
|
70
|
-
state.chunks.push("\n\n");
|
|
71
|
-
}
|
|
72
|
-
if (isReasoning) {
|
|
73
|
-
if (!state.hintPushed) {
|
|
74
|
-
state.hintPushed = true;
|
|
75
|
-
state.chunks.push(thinkingHint);
|
|
76
|
-
}
|
|
77
|
-
const text = chunk.text ?? "";
|
|
78
|
-
if (text !== "") {
|
|
79
|
-
state.chunks.push(expandLineBreaks(text));
|
|
80
|
-
}
|
|
81
|
-
state.reasoning = true;
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
const text = chunk.text ?? "";
|
|
85
|
-
state.chunks.push(text);
|
|
86
|
-
state.lastText += text;
|
|
87
|
-
state.reasoning = false;
|
|
88
|
-
}
|
|
89
|
-
state.lastIndex = chunk.index;
|
|
90
|
-
state.pushedAny = true;
|
|
91
|
-
state.lastPushAt = Date.now();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (chunk.type === "block-end") {
|
|
95
|
-
// Short blocks may arrive whole here with no preceding delta chunks.
|
|
96
|
-
// Fall back to the block text when this index never streamed deltas, so
|
|
97
|
-
// no content is silently dropped (guarded by index against duplication;
|
|
98
|
-
// each step restarts block indices, and a tool call resets `lastIndex`,
|
|
99
|
-
// so a same-index block-end after streamed deltas is reliably detected).
|
|
100
|
-
const index = chunk.index;
|
|
101
|
-
const blockType = chunk.block?.type;
|
|
102
|
-
const text = chunk.block?.text;
|
|
103
|
-
if (blockType === "reasoning" && level !== "full") {
|
|
104
|
-
// Reasoning block text is never streamed outside `full` mode.
|
|
105
|
-
if (level === "important" && !state.hintPushed) {
|
|
106
|
-
state.hintPushed = true;
|
|
107
|
-
state.chunks.push(thinkingHint);
|
|
108
|
-
state.pushedAny = true;
|
|
109
|
-
state.lastPushAt = Date.now();
|
|
110
|
-
}
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (typeof text === "string" && text.length > 0 && (index === undefined || index !== state.lastIndex)) {
|
|
114
|
-
if (state.pushedAny)
|
|
115
|
-
state.chunks.push("\n\n");
|
|
116
|
-
if (blockType === "reasoning" && !state.hintPushed) {
|
|
117
|
-
state.hintPushed = true;
|
|
118
|
-
state.chunks.push(thinkingHint);
|
|
119
|
-
}
|
|
120
|
-
state.chunks.push(blockType === "reasoning" ? expandLineBreaks(text) : text);
|
|
121
|
-
if (blockType !== "reasoning")
|
|
122
|
-
state.lastText += text;
|
|
123
|
-
state.lastIndex = index;
|
|
124
|
-
state.pushedAny = true;
|
|
125
|
-
state.reasoning = blockType === "reasoning";
|
|
126
|
-
state.lastPushAt = Date.now();
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Push a localized tool-call status line into the streaming queue so long
|
|
132
|
-
* tool-execution stretches stay visibly alive (the Web GUI shows tool calls;
|
|
133
|
-
* the chat card should too). Ends the reasoning phase and drops the block
|
|
134
|
-
* index so the next content block gets its own separator instead of stacking
|
|
135
|
-
* one on top of the tool line's own blank lines.
|
|
136
|
-
* @param state - the turn's mutable assembly state.
|
|
137
|
-
* @param label - the localized status line, e.g. "🔧 调用工具 `pwsh` — …".
|
|
138
|
-
*/
|
|
139
|
-
export function applyToolCall(state, label) {
|
|
140
|
-
state.chunks.push(`\n\n${label}\n\n`);
|
|
141
|
-
state.reasoning = false;
|
|
142
|
-
state.lastIndex = undefined;
|
|
143
|
-
state.pushedAny = true;
|
|
144
|
-
state.lastPushAt = Date.now();
|
|
145
|
-
}
|
|
146
|
-
/** Pull a short human summary out of a tool call's `arguments` JSON (whitespace folded to one line). */
|
|
147
|
-
export function toolCallSummary(argumentsJson) {
|
|
148
|
-
if (typeof argumentsJson !== "string")
|
|
149
|
-
return undefined;
|
|
150
|
-
const fold = (value) => value.replace(/\s+/g, " ").trim();
|
|
151
|
-
try {
|
|
152
|
-
const parsed = JSON.parse(argumentsJson);
|
|
153
|
-
const description = parsed?.description;
|
|
154
|
-
if (typeof description === "string" && description !== "") {
|
|
155
|
-
const folded = fold(description);
|
|
156
|
-
return folded.length > 60 ? `${folded.slice(0, 60)}…` : folded;
|
|
157
|
-
}
|
|
158
|
-
for (const key of ["file_path", "path"]) {
|
|
159
|
-
const value = parsed?.[key];
|
|
160
|
-
if (typeof value === "string" && value !== "") {
|
|
161
|
-
const folded = fold(value);
|
|
162
|
-
return folded.length > 60 ? `${folded.slice(0, 60)}…` : folded;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
// Not JSON — no summary.
|
|
168
|
-
}
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
/** Pull the first question's text out of an `ask_user_question` arguments JSON (whitespace folded to one line). */
|
|
172
|
-
export function questionTextOf(argumentsJson) {
|
|
173
|
-
if (typeof argumentsJson !== "string")
|
|
174
|
-
return "";
|
|
175
|
-
try {
|
|
176
|
-
const parsed = JSON.parse(argumentsJson);
|
|
177
|
-
const first = Array.isArray(parsed?.questions) ? parsed.questions[0] : undefined;
|
|
178
|
-
const text = typeof first?.question === "string" ? first.question : "";
|
|
179
|
-
const folded = text.replace(/\s+/g, " ").trim();
|
|
180
|
-
return folded.length > 60 ? `${folded.slice(0, 60)}…` : folded;
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
// Not JSON — no question text.
|
|
184
|
-
}
|
|
185
|
-
return "";
|
|
186
|
-
}
|
|
187
|
-
function mapReason(kind) {
|
|
188
|
-
switch (kind) {
|
|
189
|
-
case "completed":
|
|
190
|
-
case "aborted":
|
|
191
|
-
case "blocked":
|
|
192
|
-
case "error":
|
|
193
|
-
case "max-tokens":
|
|
194
|
-
case "interrupted":
|
|
195
|
-
return kind;
|
|
196
|
-
default:
|
|
197
|
-
return "unknown";
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
export function summarizeTurn(events, firstSeq) {
|
|
201
|
-
let started = false;
|
|
202
|
-
let text = "";
|
|
203
|
-
let reason = "unknown";
|
|
204
|
-
let code;
|
|
205
|
-
let message;
|
|
206
|
-
let model;
|
|
207
|
-
let inputTokens = 0;
|
|
208
|
-
let outputTokens = 0;
|
|
209
|
-
let cacheReadTokens = 0;
|
|
210
|
-
let contextSize;
|
|
211
|
-
let contextWindow;
|
|
212
|
-
let steps = 0;
|
|
213
|
-
let turnStartTime;
|
|
214
|
-
let turnEndTime;
|
|
215
|
-
for (const event of events) {
|
|
216
|
-
if (event.seq < firstSeq)
|
|
217
|
-
continue;
|
|
218
|
-
if (event.type === "turn/start") {
|
|
219
|
-
started = true;
|
|
220
|
-
turnStartTime = typeof event.time === "number" ? event.time : undefined;
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (!started)
|
|
224
|
-
continue;
|
|
225
|
-
if (event.type === "step/start")
|
|
226
|
-
steps += 1;
|
|
227
|
-
if (event.type === "request/context") {
|
|
228
|
-
const provider = typeof event.data.provider === "string" ? event.data.provider : undefined;
|
|
229
|
-
const m = typeof event.data.model === "string" ? event.data.model : undefined;
|
|
230
|
-
if (provider !== undefined && m !== undefined)
|
|
231
|
-
model = `${provider}/${m}`;
|
|
232
|
-
if (typeof event.data.contextWindow === "number")
|
|
233
|
-
contextWindow = event.data.contextWindow;
|
|
234
|
-
}
|
|
235
|
-
if (event.type === "assistant/message") {
|
|
236
|
-
const usage = event.data.usage;
|
|
237
|
-
if (usage !== undefined) {
|
|
238
|
-
inputTokens += typeof usage.inputTokens === "number" ? usage.inputTokens : 0;
|
|
239
|
-
outputTokens += typeof usage.outputTokens === "number" ? usage.outputTokens : 0;
|
|
240
|
-
cacheReadTokens += typeof usage.cacheReadTokens === "number" ? usage.cacheReadTokens : 0;
|
|
241
|
-
if (typeof usage.inputTokens === "number")
|
|
242
|
-
contextSize = usage.inputTokens;
|
|
243
|
-
}
|
|
244
|
-
const joined = event.data.message.content
|
|
245
|
-
.filter((block) => block.type === "text")
|
|
246
|
-
.map((block) => block.text)
|
|
247
|
-
.join("");
|
|
248
|
-
if (joined !== "")
|
|
249
|
-
text = joined;
|
|
250
|
-
}
|
|
251
|
-
if (event.type === "turn/end") {
|
|
252
|
-
turnEndTime = typeof event.time === "number" ? event.time : undefined;
|
|
253
|
-
reason = mapReason(event.data.reason.kind);
|
|
254
|
-
if (event.data.reason.kind === "error") {
|
|
255
|
-
code = event.data.reason.error.code;
|
|
256
|
-
message = event.data.reason.error.message;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
const elapsedMs = turnStartTime !== undefined && turnEndTime !== undefined && turnEndTime >= turnStartTime
|
|
261
|
-
? turnEndTime - turnStartTime
|
|
262
|
-
: undefined;
|
|
263
|
-
return {
|
|
264
|
-
reason,
|
|
265
|
-
text,
|
|
266
|
-
...(code === undefined ? {} : { code }),
|
|
267
|
-
...(message === undefined ? {} : { message }),
|
|
268
|
-
...(model === undefined ? {} : { model }),
|
|
269
|
-
...(inputTokens > 0 ? { inputTokens } : {}),
|
|
270
|
-
...(outputTokens > 0 ? { outputTokens } : {}),
|
|
271
|
-
...(cacheReadTokens > 0 ? { cacheReadTokens } : {}),
|
|
272
|
-
...(contextSize === undefined ? {} : { contextSize }),
|
|
273
|
-
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
274
|
-
...(steps > 0 ? { steps } : {}),
|
|
275
|
-
...(elapsedMs === undefined ? {} : { elapsedMs }),
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
function truncate(text, max = 500) {
|
|
279
|
-
return text.length <= max ? text : `${text.slice(0, max)}…`;
|
|
280
|
-
}
|
|
281
|
-
/** Classify an error message into an advice bucket (keyword-based, best-effort). */
|
|
282
|
-
export function classifyError(detail) {
|
|
283
|
-
const d = detail.toLowerCase();
|
|
284
|
-
if (/(permission|forbidden|denied|not have access|no access|unauthorized|403|sandbox|acl |isn't permitted|not permitted|access denied)/.test(d))
|
|
285
|
-
return "permission";
|
|
286
|
-
if (/(timeout|econnrefused|econnreset|enotfound|enetunreach|etimedout|socket|network|proxy|tls|certificate|fetch failed|connect |unreachable|dns)/.test(d))
|
|
287
|
-
return "network";
|
|
288
|
-
if (/(model|quota|rate ?limit|429|insufficient|balance|token(s)? (limit|length)|max_tokens|context length|credit)/.test(d))
|
|
289
|
-
return "model";
|
|
290
|
-
return "generic";
|
|
291
|
-
}
|
|
292
|
-
/** Thousands-separated token count for display. */
|
|
293
|
-
function fmtTokens(n) {
|
|
294
|
-
return n.toLocaleString("en-US");
|
|
295
|
-
}
|
|
296
|
-
function textOf(message) {
|
|
297
|
-
if (message === undefined)
|
|
298
|
-
return "";
|
|
299
|
-
return message.content
|
|
300
|
-
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
301
|
-
.map((b) => b.text ?? "")
|
|
302
|
-
.join("");
|
|
303
|
-
}
|
|
304
36
|
/** Lightweight fold of `schedule/change` events (create / delete / dispatch). */
|
|
305
37
|
function foldReminders(events) {
|
|
306
38
|
const active = new Map();
|
|
@@ -378,6 +110,7 @@ export class AgentRunner {
|
|
|
378
110
|
notifyLevel;
|
|
379
111
|
progressTimeoutMs;
|
|
380
112
|
t;
|
|
113
|
+
menu;
|
|
381
114
|
constructor(ctx, config, channel, chatKey, chatType, adapter, bindings, adapters,
|
|
382
115
|
/**
|
|
383
116
|
* Route a replayed queued message back into the runner of its own channel
|
|
@@ -404,54 +137,16 @@ export class AgentRunner {
|
|
|
404
137
|
this.notifyLevel = stored?.notifyLevel ?? config.notifyLevel;
|
|
405
138
|
this.progressTimeoutMs = stored?.progressTimeoutMs ?? config.progressTimeoutMs;
|
|
406
139
|
this.t = messages(this.language);
|
|
140
|
+
this.menu = new MenuController(this);
|
|
407
141
|
}
|
|
408
142
|
menuTitle(menuId) {
|
|
409
|
-
|
|
410
|
-
case "root":
|
|
411
|
-
return this.t.menuRoot;
|
|
412
|
-
case "workspace":
|
|
413
|
-
return this.t.menuWorkspace;
|
|
414
|
-
case "chat":
|
|
415
|
-
return this.t.menuChat;
|
|
416
|
-
case "settings":
|
|
417
|
-
return this.t.menuSettings;
|
|
418
|
-
case "model":
|
|
419
|
-
return this.t.menuModel;
|
|
420
|
-
case "reasoning":
|
|
421
|
-
return this.t.menuReasoning;
|
|
422
|
-
case "notify":
|
|
423
|
-
return this.t.menuNotify;
|
|
424
|
-
case "language":
|
|
425
|
-
return this.t.menuSettingsLanguage;
|
|
426
|
-
case "progress":
|
|
427
|
-
return this.t.progressMenuTitle;
|
|
428
|
-
}
|
|
143
|
+
return menuTitle(menuId, this.t);
|
|
429
144
|
}
|
|
430
145
|
rootMenuSections() {
|
|
431
|
-
return
|
|
432
|
-
{ title: this.t.rootSectionWorkspace, ids: ["workspace"], columnsPerRow: 1 },
|
|
433
|
-
{ title: this.t.rootSectionChat, ids: ["chat", "history"], columnsPerRow: 1 },
|
|
434
|
-
{ title: this.t.rootSectionTask, ids: ["task", "goals", "schedule"], columnsPerRow: 2 },
|
|
435
|
-
{ title: this.t.rootSectionSystem, ids: ["status", "plugins", "compact", "settings"], columnsPerRow: 2 },
|
|
436
|
-
];
|
|
146
|
+
return rootMenuSections(this.t);
|
|
437
147
|
}
|
|
438
148
|
reasonLabel(reason) {
|
|
439
|
-
|
|
440
|
-
case "completed":
|
|
441
|
-
return this.t.reasonCompleted;
|
|
442
|
-
case "aborted":
|
|
443
|
-
return this.t.reasonAborted;
|
|
444
|
-
case "blocked":
|
|
445
|
-
return this.t.reasonBlocked;
|
|
446
|
-
case "error":
|
|
447
|
-
return this.t.reasonError;
|
|
448
|
-
case "max-tokens":
|
|
449
|
-
return this.t.reasonMaxTokens;
|
|
450
|
-
case "interrupted":
|
|
451
|
-
return this.t.reasonInterrupted;
|
|
452
|
-
default:
|
|
453
|
-
return this.t.reasonUnknown;
|
|
454
|
-
}
|
|
149
|
+
return reasonLabel(reason, this.t);
|
|
455
150
|
}
|
|
456
151
|
/** Prefer the user's first DSH workspace over the process cwd as default. */
|
|
457
152
|
resolveDefaultWorkDir() {
|
|
@@ -528,16 +223,18 @@ export class AgentRunner {
|
|
|
528
223
|
else {
|
|
529
224
|
turn.milestone = this.t.toolCalling(name, undefined);
|
|
530
225
|
}
|
|
531
|
-
// `result` mode shows nothing until the final answer
|
|
532
|
-
|
|
533
|
-
|
|
226
|
+
// `result` mode shows nothing until the final answer.
|
|
227
|
+
if (this.notifyLevel === 'result')
|
|
228
|
+
return;
|
|
229
|
+
// Keep the streaming card short: push a status line only once per distinct
|
|
230
|
+
// tool (not once per call, which would log "tool #51" spam), and label it
|
|
231
|
+
// with the tool name rather than a running counter.
|
|
232
|
+
if (turn.lastToolName === name)
|
|
534
233
|
return;
|
|
535
|
-
|
|
234
|
+
turn.lastToolName = name;
|
|
235
|
+
const label = name === 'ask_user_question'
|
|
536
236
|
? this.t.questionToolCall(questionTextOf(event.data.arguments))
|
|
537
|
-
: this.t.
|
|
538
|
-
const label = name === "ask_user_question" || this.notifyLevel !== "full" || summary === undefined
|
|
539
|
-
? base
|
|
540
|
-
: `${base} — ${summary}`;
|
|
237
|
+
: this.t.toolCalling(name, this.notifyLevel === 'full' ? summary : undefined);
|
|
541
238
|
applyToolCall(turn, label);
|
|
542
239
|
}
|
|
543
240
|
}
|
|
@@ -915,7 +612,7 @@ export class AgentRunner {
|
|
|
915
612
|
this.turn = {
|
|
916
613
|
firstSeq, chunks, lastText: "",
|
|
917
614
|
reasoning: false, hintPushed: false, lastIndex: undefined, pushedAny: false,
|
|
918
|
-
startedAt: now, lastPushAt: now, toolCount: 0, contextNudged: false, compactAfterTurn: false,
|
|
615
|
+
startedAt: now, lastPushAt: now, toolCount: 0, lastToolName: undefined, contextNudged: false, compactAfterTurn: false,
|
|
919
616
|
};
|
|
920
617
|
// Liveness heartbeat: while the agent is working, keep the streaming card
|
|
921
618
|
// visibly alive even through long reasoning or tool-execution stretches
|
|
@@ -936,26 +633,29 @@ export class AgentRunner {
|
|
|
936
633
|
}, heartbeatMs)
|
|
937
634
|
: undefined;
|
|
938
635
|
// Proactive progress watchdog: when no standalone card/text has been sent
|
|
939
|
-
// for `progressTimeoutMs` (default 5 min, user-configurable),
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
636
|
+
// for `progressTimeoutMs` (default 5 min, user-configurable), re-sync the
|
|
637
|
+
// latest milestone INTO the streaming card (via its chunk stream) so a long
|
|
638
|
+
// turn never looks frozen — WITHOUT posting a separate message. The card is
|
|
639
|
+
// edited in place, keeping the chat history clean. Streaming-card heartbeats
|
|
640
|
+
// deliberately do NOT reset this: the progress watchdog only fires when no
|
|
641
|
+
// explicit milestone sync has been pushed for the configured interval.
|
|
943
642
|
const progressTimeoutMs = this.progressTimeoutMs;
|
|
944
|
-
let
|
|
643
|
+
let lastProgressNoticeAt = now;
|
|
945
644
|
const progressWatchdog = progressTimeoutMs > 0
|
|
946
645
|
? setInterval(() => {
|
|
947
646
|
const turn = this.turn;
|
|
948
647
|
if (turn === undefined)
|
|
949
648
|
return;
|
|
950
|
-
const elapsed = Date.now() -
|
|
649
|
+
const elapsed = Date.now() - lastProgressNoticeAt;
|
|
951
650
|
if (elapsed < progressTimeoutMs)
|
|
952
651
|
return;
|
|
953
|
-
|
|
652
|
+
lastProgressNoticeAt = Date.now();
|
|
954
653
|
const minutes = Math.max(1, Math.round((Date.now() - turn.startedAt) / 60_000));
|
|
955
654
|
const status = turn.milestone ?? this.t.progressThinking;
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
655
|
+
// Edit the existing streaming card in place instead of sending a new
|
|
656
|
+
// message: the milestone is appended to the same card via the chunk
|
|
657
|
+
// stream, so progress updates never clutter the chat with new bubbles.
|
|
658
|
+
turn.chunks.push(`\n\n${this.t.progressReminder(minutes, status)}\n\n`);
|
|
959
659
|
}, PROGRESS_WATCHDOG_CHECK_MS)
|
|
960
660
|
: undefined;
|
|
961
661
|
// End the chunk stream unconditionally: if `followup` / `whenIdle` /
|
|
@@ -1262,7 +962,11 @@ export class AgentRunner {
|
|
|
1262
962
|
* time, and whether compaction is advisable given context-window usage.
|
|
1263
963
|
*/
|
|
1264
964
|
async sendTurnStats(msg, outcome) {
|
|
1265
|
-
|
|
965
|
+
const hasStats = outcome.model !== undefined || outcome.inputTokens !== undefined || outcome.outputTokens !== undefined;
|
|
966
|
+
const hasResult = outcome.reason === "completed" && outcome.text !== "";
|
|
967
|
+
// No stats and no result to report — nothing to show (a bare stats card with
|
|
968
|
+
// only a duration would be noise).
|
|
969
|
+
if (!hasStats && !hasResult)
|
|
1266
970
|
return;
|
|
1267
971
|
const lines = [
|
|
1268
972
|
this.t.taskStatsHeader(outcome.elapsedMs === undefined ? "—" : this.t.taskDuration(outcome.elapsedMs)),
|
|
@@ -1281,6 +985,14 @@ export class AgentRunner {
|
|
|
1281
985
|
lines.push(this.t.taskStatsContext(`${pct}`, fmtTokens(outcome.contextWindow)));
|
|
1282
986
|
lines.push(pct >= AgentRunner.COMPACT_THRESHOLD_PCT ? this.t.taskStatsCompactSuggest : this.t.taskStatsCompactOk);
|
|
1283
987
|
}
|
|
988
|
+
// A finished task's answer must survive the end of the run: the live
|
|
989
|
+
// streaming card shows the output as it is produced, but the stats card is
|
|
990
|
+
// the durable post-task report. When the turn completed and produced text,
|
|
991
|
+
// append the result here instead of leaving a bare stats card.
|
|
992
|
+
if (hasResult) {
|
|
993
|
+
lines.push("");
|
|
994
|
+
lines.push(this.t.produced(truncate(outcome.text, 300)));
|
|
995
|
+
}
|
|
1284
996
|
const card = { markdown: lines.join("\n") };
|
|
1285
997
|
await this.adapter.sendCard(this.taskTarget(msg), card).catch(() => undefined);
|
|
1286
998
|
}
|
|
@@ -1320,7 +1032,7 @@ export class AgentRunner {
|
|
|
1320
1032
|
}
|
|
1321
1033
|
case "dir": {
|
|
1322
1034
|
if (command.path === undefined) {
|
|
1323
|
-
await this.openMenu(target, msg, "workspace", ["root"]);
|
|
1035
|
+
await this.menu.openMenu(target, msg, "workspace", ["root"]);
|
|
1324
1036
|
break;
|
|
1325
1037
|
}
|
|
1326
1038
|
const path = command.path;
|
|
@@ -1344,15 +1056,15 @@ export class AgentRunner {
|
|
|
1344
1056
|
break;
|
|
1345
1057
|
}
|
|
1346
1058
|
case "chat": {
|
|
1347
|
-
await this.openMenu(target, msg, "chat", ["root"]);
|
|
1059
|
+
await this.menu.openMenu(target, msg, "chat", ["root"]);
|
|
1348
1060
|
break;
|
|
1349
1061
|
}
|
|
1350
1062
|
case "menu": {
|
|
1351
|
-
await this.openMenu(target, msg, "root");
|
|
1063
|
+
await this.menu.openMenu(target, msg, "root");
|
|
1352
1064
|
break;
|
|
1353
1065
|
}
|
|
1354
1066
|
case "settings": {
|
|
1355
|
-
await this.openMenu(target, msg, "settings", ["root"]);
|
|
1067
|
+
await this.menu.openMenu(target, msg, "settings", ["root"]);
|
|
1356
1068
|
break;
|
|
1357
1069
|
}
|
|
1358
1070
|
case "plugins": {
|
|
@@ -1384,7 +1096,7 @@ export class AgentRunner {
|
|
|
1384
1096
|
break;
|
|
1385
1097
|
}
|
|
1386
1098
|
case "model": {
|
|
1387
|
-
await this.openMenu(target, msg, "model", ["root"]);
|
|
1099
|
+
await this.menu.openMenu(target, msg, "model", ["root"]);
|
|
1388
1100
|
break;
|
|
1389
1101
|
}
|
|
1390
1102
|
case "notify": {
|
|
@@ -1676,198 +1388,12 @@ export class AgentRunner {
|
|
|
1676
1388
|
await this.disposeAgent();
|
|
1677
1389
|
}
|
|
1678
1390
|
listWorkspaces() {
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
const key = path.replace(/[\\/]+$/, "").toLowerCase();
|
|
1686
|
-
if (seen.has(key))
|
|
1687
|
-
return;
|
|
1688
|
-
seen.add(key);
|
|
1689
|
-
out.push({ path, title: title || path });
|
|
1690
|
-
};
|
|
1691
|
-
add(this.workDir, this.t.currentDir);
|
|
1692
|
-
for (const w of registry?.list?.() ?? [])
|
|
1693
|
-
add(w.path, w.title);
|
|
1694
|
-
for (const p of this.config.workspaces)
|
|
1695
|
-
add(p, p);
|
|
1696
|
-
return out;
|
|
1697
|
-
}
|
|
1698
|
-
async openMenu(target, msg, menuId, stack = [], cardId) {
|
|
1699
|
-
const items = await this.menuItems(menuId);
|
|
1700
|
-
const options = items.map((i) => ({ id: i.id, label: i.label }));
|
|
1701
|
-
options.push({ id: "menu:exit", label: this.t.menuExit });
|
|
1702
|
-
if (stack.length > 0)
|
|
1703
|
-
options.push({ id: "menu:back", label: this.t.menuBack });
|
|
1704
|
-
// Determine columns per row based on menu type
|
|
1705
|
-
// Workspace and chat lists use 1 column (full width), others use 2 columns
|
|
1706
|
-
const columnsPerRow = (menuId === "workspace" || menuId === "chat") ? 1 : 2;
|
|
1707
|
-
const { choice, messageId } = await this.adapter.promptChoice(target, {
|
|
1708
|
-
title: this.menuTitle(menuId),
|
|
1709
|
-
options,
|
|
1710
|
-
columnsPerRow,
|
|
1711
|
-
...(menuId === "root" ? { sections: this.rootMenuSections(), footer: this.t.rootMenuFooter } : {}),
|
|
1712
|
-
}, cardId);
|
|
1713
|
-
if (choice === undefined)
|
|
1714
|
-
return;
|
|
1715
|
-
if (choice === "menu:exit") {
|
|
1716
|
-
await this.adapter.closeMenu(messageId, this.t.menuClosed);
|
|
1717
|
-
return;
|
|
1718
|
-
}
|
|
1719
|
-
if (choice === "menu:back") {
|
|
1720
|
-
const parent = stack[stack.length - 1];
|
|
1721
|
-
await this.openMenu(target, msg, parent, stack.slice(0, -1), messageId);
|
|
1722
|
-
return;
|
|
1723
|
-
}
|
|
1724
|
-
const item = items.find((i) => i.id === choice);
|
|
1725
|
-
if (item === undefined) {
|
|
1726
|
-
// The tap belonged to a previous card generation (e.g. a rapid second
|
|
1727
|
-
// tap on the old menu while the new one was still being redrawn). Don't
|
|
1728
|
-
// silently swallow it: redraw the current menu so the chain stays usable.
|
|
1729
|
-
await this.openMenu(target, msg, menuId, stack, messageId);
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
const feedback = await item.onSelect(target, msg, messageId);
|
|
1733
|
-
if (item.leaf === true) {
|
|
1734
|
-
// Every leaf press must answer visibly: send the action's result feedback
|
|
1735
|
-
// (if any) before returning to the root menu on the same card.
|
|
1736
|
-
if (feedback !== undefined && feedback !== "") {
|
|
1737
|
-
await this.adapter.sendText(target, feedback).catch(() => undefined);
|
|
1738
|
-
}
|
|
1739
|
-
await this.openMenu(target, msg, "root", [], messageId);
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
async menuItems(menuId) {
|
|
1743
|
-
switch (menuId) {
|
|
1744
|
-
case "root":
|
|
1745
|
-
return [
|
|
1746
|
-
{ id: "workspace", label: this.t.menuWorkspaceAction, onSelect: (t, m, cardId) => this.openMenu(t, m, "workspace", ["root"], cardId) },
|
|
1747
|
-
{ id: "chat", label: this.t.menuChatAction, onSelect: (t, m, cardId) => this.openMenu(t, m, "chat", ["root"], cardId) },
|
|
1748
|
-
{ id: "status", label: this.t.menuStatusAction, leaf: true, onSelect: async (t) => { await this.showStatus(t); } },
|
|
1749
|
-
{ id: "task", label: this.t.menuTaskAction, leaf: true, onSelect: async (t) => { await this.showTasks(t); } },
|
|
1750
|
-
{ id: "history", label: this.t.menuHistoryAction, leaf: true, onSelect: async (t) => { await this.showHistory(t, 10); } },
|
|
1751
|
-
{ id: "goals", label: this.t.menuGoalsAction, leaf: true, onSelect: async (t) => { await this.showGoals(t); } },
|
|
1752
|
-
{ id: "schedule", label: this.t.menuScheduleAction, leaf: true, onSelect: async (t) => { await this.showSchedule(t); } },
|
|
1753
|
-
{ id: "compact", label: this.t.menuCompactAction, leaf: true, onSelect: async (t) => { await this.compact(t); } },
|
|
1754
|
-
{ id: "plugins", label: this.t.menuPluginsAction, leaf: true, onSelect: async (t) => { await this.showPlugins(t); } },
|
|
1755
|
-
{ id: "settings", label: this.t.menuSettingsAction, onSelect: (t, m, cardId) => this.openMenu(t, m, "settings", ["root"], cardId) },
|
|
1756
|
-
];
|
|
1757
|
-
case "workspace": {
|
|
1758
|
-
const workspaces = this.listWorkspaces();
|
|
1759
|
-
return workspaces.map((w) => ({
|
|
1760
|
-
id: `dir:${w.path}`,
|
|
1761
|
-
label: `${w.path === this.workDir ? "● " : ""}${w.title}${w.title !== w.path ? ` (${w.path})` : ""}`,
|
|
1762
|
-
leaf: true,
|
|
1763
|
-
onSelect: async (t, m) => {
|
|
1764
|
-
this.workDir = w.path;
|
|
1765
|
-
await this.newChat(m);
|
|
1766
|
-
return this.t.dirSwitched(w.path);
|
|
1767
|
-
},
|
|
1768
|
-
}));
|
|
1769
|
-
}
|
|
1770
|
-
case "chat": {
|
|
1771
|
-
const binding = this.bindings.get(this.channel, this.chatKey);
|
|
1772
|
-
const active = binding?.sessionId;
|
|
1773
|
-
const hasWebMirror = binding?.webMirrorSessionId !== undefined;
|
|
1774
|
-
const items = (await this.collectWorkdirSessions()).map(({ sessionId, title }) => {
|
|
1775
|
-
const isMirrored = hasWebMirror && binding?.webMirrorSessionId === sessionId;
|
|
1776
|
-
const mirrorIndicator = isMirrored ? ` ${this.t.webMirrorIndicator}` : "";
|
|
1777
|
-
const activeIndicator = sessionId === active ? "●" : "○";
|
|
1778
|
-
return {
|
|
1779
|
-
id: `session:${sessionId}`,
|
|
1780
|
-
label: `${activeIndicator} ${title || sessionId}${mirrorIndicator}`,
|
|
1781
|
-
leaf: true,
|
|
1782
|
-
onSelect: async (t, m) => {
|
|
1783
|
-
await this.switchTo(sessionId, m.senderKey);
|
|
1784
|
-
return this.t.sessionSwitched(title || sessionId);
|
|
1785
|
-
},
|
|
1786
|
-
};
|
|
1787
|
-
});
|
|
1788
|
-
if (items.length === 0) {
|
|
1789
|
-
items.push({
|
|
1790
|
-
id: "none:history",
|
|
1791
|
-
label: this.t.noSessionsInWorkdir(this.workDir),
|
|
1792
|
-
leaf: true,
|
|
1793
|
-
onSelect: async () => this.t.noSessionsInWorkdir(this.workDir),
|
|
1794
|
-
});
|
|
1795
|
-
}
|
|
1796
|
-
items.push({
|
|
1797
|
-
id: "action:new",
|
|
1798
|
-
label: this.t.menuNewChat,
|
|
1799
|
-
leaf: true,
|
|
1800
|
-
onSelect: async (t, m, messageId) => {
|
|
1801
|
-
// Reuse the menu card for the confirm prompt so no stale card is left behind.
|
|
1802
|
-
if (await this.confirmAction(t, this.t.confirmNewText, messageId)) {
|
|
1803
|
-
await this.newChat(m);
|
|
1804
|
-
return this.t.newChatDone;
|
|
1805
|
-
}
|
|
1806
|
-
return this.t.actionCancelled;
|
|
1807
|
-
},
|
|
1808
|
-
});
|
|
1809
|
-
return items;
|
|
1810
|
-
}
|
|
1811
|
-
case "settings":
|
|
1812
|
-
return [
|
|
1813
|
-
{ id: "model", label: this.t.menuSettingsModel, onSelect: (t, m, cardId) => this.openMenu(t, m, "model", ["settings", "root"], cardId) },
|
|
1814
|
-
{ id: "reasoning", label: this.t.menuSettingsReasoning, onSelect: (t, m, cardId) => this.openMenu(t, m, "reasoning", ["settings", "root"], cardId) },
|
|
1815
|
-
{ id: "notify", label: this.t.menuSettingsNotify, onSelect: (t, m, cardId) => this.openMenu(t, m, "notify", ["settings", "root"], cardId) },
|
|
1816
|
-
{ id: "progress", label: this.t.menuSettingsProgress, onSelect: (t, m, cardId) => this.openMenu(t, m, "progress", ["settings", "root"], cardId) },
|
|
1817
|
-
{ id: "language", label: this.t.menuSettingsLanguage, onSelect: (t, m, cardId) => this.openMenu(t, m, "language", ["settings", "root"], cardId) },
|
|
1818
|
-
{ id: "overview", label: this.t.menuSettingsOverview, leaf: true, onSelect: async (t) => { await this.showSettings(t); } },
|
|
1819
|
-
];
|
|
1820
|
-
case "language":
|
|
1821
|
-
return [
|
|
1822
|
-
{
|
|
1823
|
-
id: "lang:zh",
|
|
1824
|
-
label: `${this.language === "zh" ? "● " : ""}${this.t.languageZh}`,
|
|
1825
|
-
leaf: true,
|
|
1826
|
-
onSelect: async (t, m) => { await this.setLanguage("zh", t, m); },
|
|
1827
|
-
},
|
|
1828
|
-
{
|
|
1829
|
-
id: "lang:en",
|
|
1830
|
-
label: `${this.language === "en" ? "● " : ""}${this.t.languageEn}`,
|
|
1831
|
-
leaf: true,
|
|
1832
|
-
onSelect: async (t, m) => { await this.setLanguage("en", t, m); },
|
|
1833
|
-
},
|
|
1834
|
-
];
|
|
1835
|
-
case "model":
|
|
1836
|
-
return await this.modelMenuItems();
|
|
1837
|
-
case "reasoning":
|
|
1838
|
-
return this.reasoningMenuItems();
|
|
1839
|
-
case "notify":
|
|
1840
|
-
return [
|
|
1841
|
-
{ id: "full", label: this.t.notifyFull },
|
|
1842
|
-
{ id: "important", label: this.t.notifyImportant },
|
|
1843
|
-
{ id: "result", label: this.t.notifyResult },
|
|
1844
|
-
].map((o) => ({
|
|
1845
|
-
id: `notify:${o.id}`,
|
|
1846
|
-
label: `${this.notifyLevel === o.id ? "● " : ""}${o.label}`,
|
|
1847
|
-
leaf: true,
|
|
1848
|
-
onSelect: async (t, m) => {
|
|
1849
|
-
await this.setNotifyLevel(o.id, t, m);
|
|
1850
|
-
},
|
|
1851
|
-
}));
|
|
1852
|
-
case "progress": {
|
|
1853
|
-
const presets = [
|
|
1854
|
-
{ id: "progress:0", label: this.t.progressOff, ms: 0 },
|
|
1855
|
-
{ id: "progress:120000", label: this.t.progressMinutes(2), ms: 2 * 60_000 },
|
|
1856
|
-
{ id: "progress:300000", label: this.t.progressMinutes(5), ms: 5 * 60_000 },
|
|
1857
|
-
{ id: "progress:600000", label: this.t.progressMinutes(10), ms: 10 * 60_000 },
|
|
1858
|
-
{ id: "progress:900000", label: this.t.progressMinutes(15), ms: 15 * 60_000 },
|
|
1859
|
-
{ id: "progress:1800000", label: this.t.progressMinutes(30), ms: 30 * 60_000 },
|
|
1860
|
-
];
|
|
1861
|
-
return presets.map((o) => ({
|
|
1862
|
-
id: o.id,
|
|
1863
|
-
label: `${this.progressTimeoutMs === o.ms ? "● " : ""}${o.label}`,
|
|
1864
|
-
leaf: true,
|
|
1865
|
-
onSelect: async (t, m) => {
|
|
1866
|
-
await this.setProgressTimeout(o.ms, t, m);
|
|
1867
|
-
},
|
|
1868
|
-
}));
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1391
|
+
return listWorkspaces({
|
|
1392
|
+
registry: this.ctx.get("workspaceRegistry"),
|
|
1393
|
+
workDir: this.workDir,
|
|
1394
|
+
currentDirLabel: this.t.currentDir,
|
|
1395
|
+
workspaces: this.config.workspaces,
|
|
1396
|
+
});
|
|
1871
1397
|
}
|
|
1872
1398
|
async showStatus(target) {
|
|
1873
1399
|
const agent = this.agent;
|
|
@@ -1918,6 +1444,7 @@ export class AgentRunner {
|
|
|
1918
1444
|
catch {
|
|
1919
1445
|
// Token meter unavailable: skip.
|
|
1920
1446
|
}
|
|
1447
|
+
lines.push(this.t.progressSetting(this.progressTimeoutMs === 0 ? this.t.progressOff : this.t.progressMinutes(Math.round(this.progressTimeoutMs / 60_000))));
|
|
1921
1448
|
await this.adapter.sendText(target, lines.join("\n"));
|
|
1922
1449
|
}
|
|
1923
1450
|
/** Extract the most recent turn's completion info from session events. */
|
|
@@ -1968,41 +1495,6 @@ export class AgentRunner {
|
|
|
1968
1495
|
lines.push(this.t.stateDirField(this.config.stateDir ?? ".dsh-connect"));
|
|
1969
1496
|
await this.adapter.sendText(target, lines.join("\n"));
|
|
1970
1497
|
}
|
|
1971
|
-
async modelMenuItems() {
|
|
1972
|
-
const choices = await this.listModelChoices();
|
|
1973
|
-
const current = this.defaultSelection();
|
|
1974
|
-
const items = choices.map((c) => ({
|
|
1975
|
-
id: `model:${c.provider}:${c.model}`,
|
|
1976
|
-
label: `${c.provider === current.provider && c.model === current.model ? "● " : ""}${c.name}`,
|
|
1977
|
-
leaf: true,
|
|
1978
|
-
onSelect: async (t, m) => {
|
|
1979
|
-
await this.setModel(c.provider, c.model, m);
|
|
1980
|
-
return this.t.modelSet(c.name);
|
|
1981
|
-
},
|
|
1982
|
-
}));
|
|
1983
|
-
if (items.length === 0) {
|
|
1984
|
-
items.push({ id: "model:none", label: this.t.noModelsFound, leaf: true, onSelect: async () => this.t.noModelsFound });
|
|
1985
|
-
}
|
|
1986
|
-
return items;
|
|
1987
|
-
}
|
|
1988
|
-
async listModelChoices() {
|
|
1989
|
-
const llm = this.ctx.get("llm");
|
|
1990
|
-
const providers = llm?.listProviders?.() ?? [];
|
|
1991
|
-
const out = [];
|
|
1992
|
-
for (const p of providers) {
|
|
1993
|
-
let models = [];
|
|
1994
|
-
try {
|
|
1995
|
-
models = (await llm?.listModels?.(p.id)) ?? [];
|
|
1996
|
-
}
|
|
1997
|
-
catch {
|
|
1998
|
-
// Skip providers whose catalog cannot be listed.
|
|
1999
|
-
}
|
|
2000
|
-
for (const m of models) {
|
|
2001
|
-
out.push({ provider: p.id, model: m.id, name: this.t.modelName(m.name || m.id, p.name || p.id) });
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
return out;
|
|
2005
|
-
}
|
|
2006
1498
|
async setModel(provider, model, msg) {
|
|
2007
1499
|
const svc = this.ctx.get("agentDefaultModel");
|
|
2008
1500
|
const cur = this.defaultSelection();
|
|
@@ -2013,37 +1505,6 @@ export class AgentRunner {
|
|
|
2013
1505
|
});
|
|
2014
1506
|
await this.newChat(msg);
|
|
2015
1507
|
}
|
|
2016
|
-
reasoningMenuItems() {
|
|
2017
|
-
const current = this.defaultSelection();
|
|
2018
|
-
const efforts = [
|
|
2019
|
-
{ id: "low", name: this.t.effortLow },
|
|
2020
|
-
{ id: "medium", name: this.t.effortMedium },
|
|
2021
|
-
{ id: "high", name: this.t.effortHigh },
|
|
2022
|
-
];
|
|
2023
|
-
const items = [
|
|
2024
|
-
{
|
|
2025
|
-
id: "effort:default",
|
|
2026
|
-
label: `${current.reasoningEffort === undefined ? "● " : ""}${this.t.effortDefault}`,
|
|
2027
|
-
leaf: true,
|
|
2028
|
-
onSelect: async (t, m) => {
|
|
2029
|
-
await this.setReasoning(undefined, m);
|
|
2030
|
-
return this.t.reasoningSet(this.t.effortDefault);
|
|
2031
|
-
},
|
|
2032
|
-
},
|
|
2033
|
-
];
|
|
2034
|
-
for (const e of efforts) {
|
|
2035
|
-
items.push({
|
|
2036
|
-
id: `effort:${e.id}`,
|
|
2037
|
-
label: `${e.id === current.reasoningEffort ? "● " : ""}${e.name}`,
|
|
2038
|
-
leaf: true,
|
|
2039
|
-
onSelect: async (t, m) => {
|
|
2040
|
-
await this.setReasoning(e.id, m);
|
|
2041
|
-
return this.t.reasoningSet(e.name);
|
|
2042
|
-
},
|
|
2043
|
-
});
|
|
2044
|
-
}
|
|
2045
|
-
return items;
|
|
2046
|
-
}
|
|
2047
1508
|
async setReasoning(effort, msg) {
|
|
2048
1509
|
const svc = this.ctx.get("agentDefaultModel");
|
|
2049
1510
|
const cur = this.defaultSelection();
|
|
@@ -2154,14 +1615,10 @@ export class AgentRunner {
|
|
|
2154
1615
|
}
|
|
2155
1616
|
/** Show the progress-interval picker (used by the `/progress` command). */
|
|
2156
1617
|
async openProgressPicker(target, msg) {
|
|
2157
|
-
const options =
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
{ id: "progress:600000", label: this.t.progressMinutes(10) },
|
|
2162
|
-
{ id: "progress:900000", label: this.t.progressMinutes(15) },
|
|
2163
|
-
{ id: "progress:1800000", label: this.t.progressMinutes(30) },
|
|
2164
|
-
];
|
|
1618
|
+
const options = PROGRESS_PRESET_MS.map((ms) => ({
|
|
1619
|
+
id: `progress:${ms}`,
|
|
1620
|
+
label: ms === 0 ? this.t.progressOff : this.t.progressMinutes(ms / 60_000),
|
|
1621
|
+
}));
|
|
2165
1622
|
const { choice } = await this.adapter.promptChoice(target, { title: this.t.progressMenuTitle, options });
|
|
2166
1623
|
if (choice === undefined || !choice.startsWith("progress:"))
|
|
2167
1624
|
return;
|
|
@@ -2304,18 +1761,7 @@ export class AgentRunner {
|
|
|
2304
1761
|
await this.adapter.sendText(target, lines.join("\n"));
|
|
2305
1762
|
}
|
|
2306
1763
|
goalPhaseLabel(phase) {
|
|
2307
|
-
|
|
2308
|
-
case "active":
|
|
2309
|
-
return this.t.goalPhaseActive;
|
|
2310
|
-
case "paused":
|
|
2311
|
-
return this.t.goalPhasePaused;
|
|
2312
|
-
case "blocked":
|
|
2313
|
-
return this.t.goalPhaseBlocked;
|
|
2314
|
-
case "complete":
|
|
2315
|
-
return this.t.goalPhaseComplete;
|
|
2316
|
-
default:
|
|
2317
|
-
return phase;
|
|
2318
|
-
}
|
|
1764
|
+
return goalPhaseLabel(phase, this.t);
|
|
2319
1765
|
}
|
|
2320
1766
|
async showSchedule(target) {
|
|
2321
1767
|
// Agent-level reminders (session `schedule` tool) + persistent chat-level
|