zidane 5.3.0 → 5.3.1
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/{agent-CYpPKn5Z.d.ts → agent-bKs7MRT2.d.ts} +430 -5
- package/dist/agent-bKs7MRT2.d.ts.map +1 -0
- package/dist/chat.d.ts +310 -6
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +2 -2
- package/dist/{errors-COmsomd5.js → errors-Byb0F8B9.js} +44 -2
- package/dist/errors-Byb0F8B9.js.map +1 -0
- package/dist/{index-D-cTScN3.d.ts → index-BlMvPh9X.d.ts} +57 -10
- package/dist/index-BlMvPh9X.d.ts.map +1 -0
- package/dist/{index-Cc-q1hLT.d.ts → index-CTmNaIDb.d.ts} +2 -2
- package/dist/{index-Cc-q1hLT.d.ts.map → index-CTmNaIDb.d.ts.map} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +10 -10
- package/dist/{interpolate-BhmHKD6x.js → interpolate-ERgZUxgg.js} +2 -2
- package/dist/{interpolate-BhmHKD6x.js.map → interpolate-ERgZUxgg.js.map} +1 -1
- package/dist/{login-BXVt5wuA.js → login-CNS9_8Ue.js} +3 -3
- package/dist/{login-BXVt5wuA.js.map → login-CNS9_8Ue.js.map} +1 -1
- package/dist/{mcp-B1psg7jf.js → mcp-ZsSFo4Dp.js} +2 -2
- package/dist/{mcp-B1psg7jf.js.map → mcp-ZsSFo4Dp.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{messages-DsbMYNmt.js → messages-D0xT979U.js} +631 -68
- package/dist/messages-D0xT979U.js.map +1 -0
- package/dist/{presets-tvD28pCu.js → presets-h5i3kpOP.js} +29 -10
- package/dist/presets-h5i3kpOP.js.map +1 -0
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +1 -1
- package/dist/{providers-v1Rn2rqG.js → providers-x3LZByR5.js} +38 -6
- package/dist/providers-x3LZByR5.js.map +1 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -3
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.js +1 -1
- package/dist/{session-DOJgRXvF.js → session-BHZwxmfr.js} +2 -2
- package/dist/{session-DOJgRXvF.js.map → session-BHZwxmfr.js.map} +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -1
- package/dist/{tools-CMVruxF0.js → tools-CWEDS2ZT.js} +380 -48
- package/dist/tools-CWEDS2ZT.js.map +1 -0
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +1 -1
- package/dist/{transcript-anchors-eyhlGeBI.d.ts → transcript-anchors-DOUqyvXR.d.ts} +28 -4
- package/dist/transcript-anchors-DOUqyvXR.d.ts.map +1 -0
- package/dist/tui.d.ts +29 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +365 -80
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-Y7e15gJf.js → turn-operations-D9HvatsR.js} +678 -33
- package/dist/turn-operations-D9HvatsR.js.map +1 -0
- package/dist/types-IcokUOyC.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/dist/agent-CYpPKn5Z.d.ts.map +0 -1
- package/dist/errors-COmsomd5.js.map +0 -1
- package/dist/index-D-cTScN3.d.ts.map +0 -1
- package/dist/messages-DsbMYNmt.js.map +0 -1
- package/dist/presets-tvD28pCu.js.map +0 -1
- package/dist/providers-v1Rn2rqG.js.map +0 -1
- package/dist/tools-CMVruxF0.js.map +0 -1
- package/dist/transcript-anchors-eyhlGeBI.d.ts.map +0 -1
- package/dist/turn-operations-Y7e15gJf.js.map +0 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { a as multiEdit, c as grep, d as resolveOldString, f as styleReplacementForVia, i as readFile$1, l as glob, n as createSpawnTool, o as listFiles, r as shell, t as writeFile$1, u as edit } from "./tools-
|
|
2
|
-
import {
|
|
1
|
+
import { a as multiEdit, c as grep, d as resolveOldString, f as styleReplacementForVia, i as readFile$1, l as glob, n as createSpawnTool, o as listFiles, r as shell, t as writeFile$1, u as edit } from "./tools-CWEDS2ZT.js";
|
|
2
|
+
import { s as errorMessage } from "./errors-Byb0F8B9.js";
|
|
3
3
|
import { n as toolResultToText } from "./types-IcokUOyC.js";
|
|
4
|
-
import { r as normalizeMcpServers } from "./mcp-
|
|
5
|
-
import { a as discoverSkills } from "./interpolate-
|
|
4
|
+
import { r as normalizeMcpServers } from "./mcp-ZsSFo4Dp.js";
|
|
5
|
+
import { a as discoverSkills } from "./interpolate-ERgZUxgg.js";
|
|
6
6
|
import { n as formatTokenUsage } from "./stats-DgOvY7wd.js";
|
|
7
|
-
import { n as definePreset } from "./presets-
|
|
8
|
-
import { a as writeFileAtomic, i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-
|
|
7
|
+
import { n as definePreset, t as composePresets } from "./presets-h5i3kpOP.js";
|
|
8
|
+
import { a as writeFileAtomic, i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-x3LZByR5.js";
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
10
|
import { readdir, stat, writeFile } from "node:fs/promises";
|
|
11
11
|
import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
@@ -253,6 +253,581 @@ function joinPrompt(parts) {
|
|
|
253
253
|
return parts.filter((p) => typeof p === "string" && p.length > 0).join("\n\n");
|
|
254
254
|
}
|
|
255
255
|
//#endregion
|
|
256
|
+
//#region src/chat/todos.ts
|
|
257
|
+
const TODOWRITE_TOOL = "todowrite";
|
|
258
|
+
const TODOREAD_TOOL = "todoread";
|
|
259
|
+
/** True when `name` is one of the todo tool canonical names. */
|
|
260
|
+
function isTodoTool(name) {
|
|
261
|
+
return name === "todowrite" || name === "todoread";
|
|
262
|
+
}
|
|
263
|
+
/** `session.metadata[TODOS_METADATA_KEY]: TodosBag` — see {@link TodosBag}. */
|
|
264
|
+
const TODOS_METADATA_KEY = "todos";
|
|
265
|
+
/** `session.metadata[TODO_WRITE_COUNTS_METADATA_KEY]: CountsBag` — mirrors `TodosBag` shape. */
|
|
266
|
+
const TODO_WRITE_COUNTS_METADATA_KEY = "todoWriteCounts";
|
|
267
|
+
const TODO_STATUS_VALUES = [
|
|
268
|
+
"pending",
|
|
269
|
+
"in_progress",
|
|
270
|
+
"completed",
|
|
271
|
+
"cancelled"
|
|
272
|
+
];
|
|
273
|
+
const TODO_STATUS_GLYPHS = {
|
|
274
|
+
pending: "☐",
|
|
275
|
+
in_progress: "◐",
|
|
276
|
+
completed: "☑",
|
|
277
|
+
cancelled: "☒"
|
|
278
|
+
};
|
|
279
|
+
const WRITE_DESCRIPTION = "Replace the active task list. Pass the **full** list of items every call — there is no partial update. The list persists across user prompts in the same session, so an aborted run with `in_progress` items reads back on the next `todoread` and you can pick up where you left off. When every item is `completed`, the list auto-clears so the next prompt starts fresh.\n\nUse the `status` field to track progress: `pending` (queued), `in_progress` (currently working), `completed` (done), `cancelled` (no longer relevant).\n\nOnly checkpoint at significant transitions:\n 1. When the user gives you a multi-step task — write the initial plan with everything `pending`.\n 2. When you transition between steps — mark the previous one `completed` and the next one `in_progress`.\n 3. When the user asks for the current status — re-emit the list unchanged so they can see it.\n\nDo NOT call this on every action. The list is for the user's situational awareness, not for self-narrating.";
|
|
280
|
+
const READ_DESCRIPTION = "Return the current active task list. Returns an empty list if nothing has been written yet, or if the previous batch was auto-cleared after all items completed. Use sparingly — you already see the latest list in your own `todowrite` tool_result above.";
|
|
281
|
+
function defaultReminder(count) {
|
|
282
|
+
return `(You've called todowrite ${count} times. Make sure each checkpoint reflects real progress; avoid re-planning every step.)`;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* `true` when the run with `runId` is a subagent (has a `parentRunId`).
|
|
286
|
+
* Top-level runs return `false`; an unknown / missing run also returns
|
|
287
|
+
* `false` (defensive — should never happen in practice, but treating an
|
|
288
|
+
* orphan as top-level is the right fallback since the session slot is
|
|
289
|
+
* the more general bucket).
|
|
290
|
+
*/
|
|
291
|
+
function isSubagentRun(session, runId) {
|
|
292
|
+
return !!session.runs.find((r) => r.id === runId)?.parentRunId;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Read the active list for `runId`. Top-level runs see the session-shared
|
|
296
|
+
* slot; subagent runs see their own. Returns a fresh `[]` (not stored in
|
|
297
|
+
* metadata) when no slot exists — the caller can mutate the result
|
|
298
|
+
* without affecting state.
|
|
299
|
+
*/
|
|
300
|
+
function getTodosForRun(session, runId) {
|
|
301
|
+
const bag = readTodosBag(session);
|
|
302
|
+
if (isSubagentRun(session, runId)) {
|
|
303
|
+
const items = bag.byRun?.[runId];
|
|
304
|
+
return Array.isArray(items) ? [...items] : [];
|
|
305
|
+
}
|
|
306
|
+
return Array.isArray(bag.session) ? [...bag.session] : [];
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Replace the active list for `runId`. Routes to the session-shared slot
|
|
310
|
+
* for top-level runs and to the per-run slot for subagents. Empty arrays
|
|
311
|
+
* delete the slot entirely so persisted metadata stays clean.
|
|
312
|
+
*
|
|
313
|
+
* Side effect: a **non-empty** write also stashes the same list into
|
|
314
|
+
* `bag.archive` (same routing). Empty writes never touch the archive —
|
|
315
|
+
* clearing the live list (whether explicitly or via the tool's auto-
|
|
316
|
+
* clear) preserves the "last shown" snapshot so UI surfaces can keep
|
|
317
|
+
* rendering what was just completed. See {@link getArchivedTodosForRun}.
|
|
318
|
+
*/
|
|
319
|
+
function setTodosForRun(session, runId, items) {
|
|
320
|
+
const bag = readTodosBag(session);
|
|
321
|
+
const normalized = items.map(normalizeItem);
|
|
322
|
+
const subagent = isSubagentRun(session, runId);
|
|
323
|
+
if (subagent) {
|
|
324
|
+
const byRun = { ...bag.byRun ?? {} };
|
|
325
|
+
if (normalized.length === 0) delete byRun[runId];
|
|
326
|
+
else byRun[runId] = normalized;
|
|
327
|
+
bag.byRun = byRun;
|
|
328
|
+
} else if (normalized.length === 0) delete bag.session;
|
|
329
|
+
else bag.session = normalized;
|
|
330
|
+
if (normalized.length > 0) {
|
|
331
|
+
const archive = { ...bag.archive ?? {} };
|
|
332
|
+
if (subagent) {
|
|
333
|
+
const archiveByRun = { ...archive.byRun ?? {} };
|
|
334
|
+
archiveByRun[runId] = normalized;
|
|
335
|
+
archive.byRun = archiveByRun;
|
|
336
|
+
} else archive.session = normalized;
|
|
337
|
+
bag.archive = archive;
|
|
338
|
+
}
|
|
339
|
+
writeTodosBag(session, bag);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Read the archived (most-recent non-empty) list for `runId`. Same slot
|
|
343
|
+
* routing as {@link getTodosForRun}, but reads `bag.archive` instead of
|
|
344
|
+
* the live slot. Returns a fresh `[]` when no archive exists.
|
|
345
|
+
*
|
|
346
|
+
* UI surfaces use this to keep showing "what was just completed" after
|
|
347
|
+
* the live list auto-clears. The model never sees the archive — it's a
|
|
348
|
+
* read-only sidecar for human-facing rendering.
|
|
349
|
+
*/
|
|
350
|
+
function getArchivedTodosForRun(session, runId) {
|
|
351
|
+
const archive = readTodosBag(session).archive;
|
|
352
|
+
if (!archive) return [];
|
|
353
|
+
if (isSubagentRun(session, runId)) {
|
|
354
|
+
const items = archive.byRun?.[runId];
|
|
355
|
+
return Array.isArray(items) ? [...items] : [];
|
|
356
|
+
}
|
|
357
|
+
return Array.isArray(archive.session) ? [...archive.session] : [];
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Reconcile `session.metadata.todos.byRun` (live + archive) against
|
|
361
|
+
* `session.runs`. Drops subagent slots whose runId isn't in the run
|
|
362
|
+
* list. The top-level `session` slot is unaffected — it's session-
|
|
363
|
+
* scoped, not per-run, so there's no orphan to GC.
|
|
364
|
+
*
|
|
365
|
+
* Useful after `session.setRuns()` (fork / restore) or to GC stale
|
|
366
|
+
* metadata mutated by an external caller. Also prunes the parallel
|
|
367
|
+
* counter bag so the two don't drift.
|
|
368
|
+
*
|
|
369
|
+
* `dropped` reports the live-byRun orphans only (archive-only orphans
|
|
370
|
+
* are silently swept). The live half is the contract callers actually
|
|
371
|
+
* observe.
|
|
372
|
+
*/
|
|
373
|
+
function pruneTodosByRun(session) {
|
|
374
|
+
const validRunIds = new Set(session.runs.map((r) => r.id));
|
|
375
|
+
const dropped = [];
|
|
376
|
+
const bag = readTodosBag(session);
|
|
377
|
+
let bagChanged = false;
|
|
378
|
+
if (bag.byRun) {
|
|
379
|
+
const nextByRun = {};
|
|
380
|
+
for (const [runId, items] of Object.entries(bag.byRun)) if (validRunIds.has(runId)) nextByRun[runId] = items;
|
|
381
|
+
else {
|
|
382
|
+
dropped.push(runId);
|
|
383
|
+
bagChanged = true;
|
|
384
|
+
}
|
|
385
|
+
if (Object.keys(nextByRun).length !== Object.keys(bag.byRun).length) bag.byRun = nextByRun;
|
|
386
|
+
}
|
|
387
|
+
if (bag.archive?.byRun) {
|
|
388
|
+
const nextByRun = {};
|
|
389
|
+
let archiveChanged = false;
|
|
390
|
+
for (const [runId, items] of Object.entries(bag.archive.byRun)) if (validRunIds.has(runId)) nextByRun[runId] = items;
|
|
391
|
+
else archiveChanged = true;
|
|
392
|
+
if (archiveChanged) {
|
|
393
|
+
bag.archive = {
|
|
394
|
+
...bag.archive,
|
|
395
|
+
byRun: nextByRun
|
|
396
|
+
};
|
|
397
|
+
bagChanged = true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (bagChanged) writeTodosBag(session, bag);
|
|
401
|
+
const counts = readCountsBag(session);
|
|
402
|
+
if (counts.byRun) {
|
|
403
|
+
const nextByRun = {};
|
|
404
|
+
let changed = false;
|
|
405
|
+
for (const [runId, n] of Object.entries(counts.byRun)) if (validRunIds.has(runId)) nextByRun[runId] = n;
|
|
406
|
+
else changed = true;
|
|
407
|
+
if (changed) {
|
|
408
|
+
counts.byRun = nextByRun;
|
|
409
|
+
writeCountsBag(session, counts);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return { dropped };
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Build a `Preset` carrying the `{ todowrite, todoread }` tool pair.
|
|
416
|
+
* Identical-payload dedup is handled inside the tool body — the
|
|
417
|
+
* comparison resolves against the active slot (session-shared for
|
|
418
|
+
* top-level runs, per-run for subagents) and intentionally does NOT
|
|
419
|
+
* plumb through `behavior.dedupTools`. See the `dedupIdentical` option
|
|
420
|
+
* doc for the rationale.
|
|
421
|
+
*
|
|
422
|
+
* By default the preset adds **no** `toolBudgets` entry — `todowrite`
|
|
423
|
+
* is a state-transition tool, so a hard cap is the wrong abstraction
|
|
424
|
+
* (it punishes the legitimate "finish the N-item plan" path). Hosts
|
|
425
|
+
* that want a ceiling pass `maxWritesPerRun: N` explicitly; the
|
|
426
|
+
* factory then plumbs a `behavior.toolBudgets.todowrite` entry
|
|
427
|
+
* through.
|
|
428
|
+
*
|
|
429
|
+
* Returning a `Preset` (not a bare tool map) lets the result flow
|
|
430
|
+
* through {@link composePresets} unchanged — todos compose with any
|
|
431
|
+
* other preset the same way every other preset does. `toolBudgets` is
|
|
432
|
+
* a tool-name-keyed record that `composePresets` deep-merges, so a
|
|
433
|
+
* caller's custom budget entries for other tools survive the
|
|
434
|
+
* layering, and a caller's override for `todowrite` itself wins by
|
|
435
|
+
* being placed later in the chain.
|
|
436
|
+
*
|
|
437
|
+
* ```ts
|
|
438
|
+
* import { basic, composePresets } from 'zidane/presets'
|
|
439
|
+
* import { createTodoTools } from 'zidane/chat'
|
|
440
|
+
*
|
|
441
|
+
* // Default: no budget — relies on the in-band reminder + dedup.
|
|
442
|
+
* createAgent({ ...composePresets(basic, createTodoTools()), provider })
|
|
443
|
+
*
|
|
444
|
+
* // Opt-in hard cap: refuse > 20 writes per run.
|
|
445
|
+
* createAgent({
|
|
446
|
+
* ...composePresets(basic, createTodoTools({ maxWritesPerRun: 20 })),
|
|
447
|
+
* provider,
|
|
448
|
+
* })
|
|
449
|
+
* ```
|
|
450
|
+
*
|
|
451
|
+
* For the trivial "just add the tools to an existing config" case, plain
|
|
452
|
+
* spread is also fine since the returned `Preset` only sets `tools`
|
|
453
|
+
* (and `behavior` when a budget is opted in):
|
|
454
|
+
*
|
|
455
|
+
* ```ts
|
|
456
|
+
* createAgent({ ...basic, ...createTodoTools(), provider })
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
function createTodoTools(options = {}) {
|
|
460
|
+
const maxItems = options.maxItems ?? 100;
|
|
461
|
+
const remindAfter = options.remindAfter ?? 3;
|
|
462
|
+
const reminderText = options.reminderText ?? ((count, _items) => defaultReminder(count));
|
|
463
|
+
const dedupIdentical = options.dedupIdentical ?? true;
|
|
464
|
+
const maxWritesPerRun = options.maxWritesPerRun ?? 0;
|
|
465
|
+
const onMaxWrites = options.onMaxWrites ?? "steer";
|
|
466
|
+
const tools = {
|
|
467
|
+
[TODOWRITE_TOOL]: createTodoWriteTool({
|
|
468
|
+
maxItems,
|
|
469
|
+
remindAfter,
|
|
470
|
+
reminderText,
|
|
471
|
+
dedupIdentical,
|
|
472
|
+
description: options.writeDescription ?? WRITE_DESCRIPTION
|
|
473
|
+
}),
|
|
474
|
+
[TODOREAD_TOOL]: createTodoReadTool({ description: options.readDescription ?? READ_DESCRIPTION })
|
|
475
|
+
};
|
|
476
|
+
const behavior = {};
|
|
477
|
+
if (maxWritesPerRun > 0) behavior.toolBudgets = { [TODOWRITE_TOOL]: {
|
|
478
|
+
max: maxWritesPerRun,
|
|
479
|
+
onExceed: onMaxWrites
|
|
480
|
+
} };
|
|
481
|
+
return definePreset(Object.keys(behavior).length > 0 ? {
|
|
482
|
+
tools,
|
|
483
|
+
behavior
|
|
484
|
+
} : { tools });
|
|
485
|
+
}
|
|
486
|
+
function createTodoWriteTool(opts) {
|
|
487
|
+
return {
|
|
488
|
+
spec: {
|
|
489
|
+
name: TODOWRITE_TOOL,
|
|
490
|
+
description: opts.description,
|
|
491
|
+
inputSchema: {
|
|
492
|
+
type: "object",
|
|
493
|
+
properties: { todos: {
|
|
494
|
+
type: "array",
|
|
495
|
+
description: "The complete task list. Replaces the prior list in full.",
|
|
496
|
+
maxItems: opts.maxItems,
|
|
497
|
+
items: {
|
|
498
|
+
type: "object",
|
|
499
|
+
properties: {
|
|
500
|
+
id: {
|
|
501
|
+
type: "string",
|
|
502
|
+
description: "Stable identifier (the model picks)."
|
|
503
|
+
},
|
|
504
|
+
content: {
|
|
505
|
+
type: "string",
|
|
506
|
+
description: "One-line task summary."
|
|
507
|
+
},
|
|
508
|
+
status: {
|
|
509
|
+
type: "string",
|
|
510
|
+
enum: [...TODO_STATUS_VALUES],
|
|
511
|
+
description: "`pending`, `in_progress`, `completed`, or `cancelled`."
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
required: [
|
|
515
|
+
"id",
|
|
516
|
+
"content",
|
|
517
|
+
"status"
|
|
518
|
+
]
|
|
519
|
+
}
|
|
520
|
+
} },
|
|
521
|
+
required: ["todos"]
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
async execute(input, ctx) {
|
|
525
|
+
const { session, runId } = requireSessionAndRun(ctx, TODOWRITE_TOOL);
|
|
526
|
+
const rawItems = Array.isArray(input.todos) ? input.todos : [];
|
|
527
|
+
const items = sanitizeItems(rawItems, opts.maxItems);
|
|
528
|
+
const dropped = rawItems.length - items.length;
|
|
529
|
+
const byId = /* @__PURE__ */ new Map();
|
|
530
|
+
for (const item of items) byId.set(item.id, item);
|
|
531
|
+
const normalized = [...byId.values()];
|
|
532
|
+
const current = getTodosForRun(session, runId);
|
|
533
|
+
const unchanged = opts.dedupIdentical && todosEqual(current, normalized);
|
|
534
|
+
const count = incrementCount(session, runId);
|
|
535
|
+
const allCompleted = normalized.length > 0 && normalized.every((t) => t.status === "completed");
|
|
536
|
+
if (!unchanged) if (allCompleted) {
|
|
537
|
+
setTodosForRun(session, runId, normalized);
|
|
538
|
+
setTodosForRun(session, runId, []);
|
|
539
|
+
} else setTodosForRun(session, runId, normalized);
|
|
540
|
+
return formatWriteResult({
|
|
541
|
+
items: normalized,
|
|
542
|
+
dropped,
|
|
543
|
+
count,
|
|
544
|
+
unchanged,
|
|
545
|
+
cleared: allCompleted && !unchanged,
|
|
546
|
+
opts
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function formatWriteResult(input) {
|
|
552
|
+
const { items, dropped, count, unchanged, cleared, opts } = input;
|
|
553
|
+
const lines = [];
|
|
554
|
+
const n = items.length;
|
|
555
|
+
const suffix = n === 1 ? "" : "s";
|
|
556
|
+
if (cleared) lines.push(`Marked ${n} item${suffix} complete — list cleared.`);
|
|
557
|
+
else if (unchanged) lines.push(`No change — ${n} todo item${suffix} already tracked.`);
|
|
558
|
+
else lines.push(`Updated ${n} todo item${suffix}.`);
|
|
559
|
+
if (!cleared) {
|
|
560
|
+
const tally = summarizeStatuses(items);
|
|
561
|
+
if (tally) lines.push(tally);
|
|
562
|
+
}
|
|
563
|
+
if (dropped > 0) lines.push(`Dropped ${dropped} malformed item${dropped === 1 ? "" : "s"}.`);
|
|
564
|
+
const reminder = opts.remindAfter > 0 && count >= opts.remindAfter ? opts.reminderText(count, items) : void 0;
|
|
565
|
+
if (reminder && reminder.length > 0) lines.push(reminder);
|
|
566
|
+
return lines.join("\n");
|
|
567
|
+
}
|
|
568
|
+
function createTodoReadTool(opts) {
|
|
569
|
+
return {
|
|
570
|
+
spec: {
|
|
571
|
+
name: TODOREAD_TOOL,
|
|
572
|
+
description: opts.description,
|
|
573
|
+
inputSchema: {
|
|
574
|
+
type: "object",
|
|
575
|
+
properties: {}
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
async execute(_input, ctx) {
|
|
579
|
+
const { session, runId } = requireSessionAndRun(ctx, TODOREAD_TOOL);
|
|
580
|
+
const items = getTodosForRun(session, runId);
|
|
581
|
+
if (items.length === 0) return "No todos yet — call todowrite to start tracking tasks.";
|
|
582
|
+
return JSON.stringify({ todos: items });
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Both tool bodies need a `Session` + `runId`. Centralize the guards so the
|
|
588
|
+
* error messages stay symmetric and one fix lands in both places.
|
|
589
|
+
*/
|
|
590
|
+
function requireSessionAndRun(ctx, toolName) {
|
|
591
|
+
if (!ctx.session) throw new Error(`${toolName}: no session on tool context — todos require a session via createSession().`);
|
|
592
|
+
if (!ctx.runId) throw new Error(`${toolName}: no runId on tool context.`);
|
|
593
|
+
return {
|
|
594
|
+
session: ctx.session,
|
|
595
|
+
runId: ctx.runId
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/** Read the todos bag from session metadata, normalizing the live shape. */
|
|
599
|
+
function readTodosBag(session) {
|
|
600
|
+
const raw = session.metadata[TODOS_METADATA_KEY];
|
|
601
|
+
if (!isPlainObject(raw)) return {};
|
|
602
|
+
const archive = isPlainObject(raw.archive) ? {
|
|
603
|
+
session: Array.isArray(raw.archive.session) ? raw.archive.session : void 0,
|
|
604
|
+
byRun: isPlainObject(raw.archive.byRun) ? raw.archive.byRun : void 0
|
|
605
|
+
} : void 0;
|
|
606
|
+
return {
|
|
607
|
+
session: Array.isArray(raw.session) ? raw.session : void 0,
|
|
608
|
+
byRun: isPlainObject(raw.byRun) ? raw.byRun : void 0,
|
|
609
|
+
archive
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function writeTodosBag(session, bag) {
|
|
613
|
+
const clean = {};
|
|
614
|
+
if (bag.session && bag.session.length > 0) clean.session = bag.session;
|
|
615
|
+
if (bag.byRun && Object.keys(bag.byRun).length > 0) clean.byRun = bag.byRun;
|
|
616
|
+
if (bag.archive) {
|
|
617
|
+
const archive = {};
|
|
618
|
+
if (bag.archive.session && bag.archive.session.length > 0) archive.session = bag.archive.session;
|
|
619
|
+
if (bag.archive.byRun && Object.keys(bag.archive.byRun).length > 0) archive.byRun = bag.archive.byRun;
|
|
620
|
+
if (archive.session || archive.byRun) clean.archive = archive;
|
|
621
|
+
}
|
|
622
|
+
session.setMeta(TODOS_METADATA_KEY, clean);
|
|
623
|
+
}
|
|
624
|
+
function readCountsBag(session) {
|
|
625
|
+
const raw = session.metadata[TODO_WRITE_COUNTS_METADATA_KEY];
|
|
626
|
+
if (!isPlainObject(raw)) return {};
|
|
627
|
+
return {
|
|
628
|
+
session: typeof raw.session === "number" ? raw.session : void 0,
|
|
629
|
+
byRun: isPlainObject(raw.byRun) ? raw.byRun : void 0
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function writeCountsBag(session, bag) {
|
|
633
|
+
const clean = {};
|
|
634
|
+
if (typeof bag.session === "number" && bag.session > 0) clean.session = bag.session;
|
|
635
|
+
if (bag.byRun && Object.keys(bag.byRun).length > 0) clean.byRun = bag.byRun;
|
|
636
|
+
session.setMeta(TODO_WRITE_COUNTS_METADATA_KEY, clean);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Bump the call counter for the active slot and return the new value.
|
|
640
|
+
* Top-level runs increment `bag.session` (cumulative across prompts);
|
|
641
|
+
* subagent runs increment their own entry under `bag.byRun`. Same
|
|
642
|
+
* keying as {@link setTodosForRun} so the counter and the data move
|
|
643
|
+
* together — a freshly seeded run's count starts at 1 on its first
|
|
644
|
+
* write, a continuing top-level run's count picks up from where the
|
|
645
|
+
* prior prompt left off.
|
|
646
|
+
*/
|
|
647
|
+
function incrementCount(session, runId) {
|
|
648
|
+
const bag = readCountsBag(session);
|
|
649
|
+
let next;
|
|
650
|
+
if (isSubagentRun(session, runId)) {
|
|
651
|
+
const byRun = { ...bag.byRun ?? {} };
|
|
652
|
+
next = (byRun[runId] ?? 0) + 1;
|
|
653
|
+
byRun[runId] = next;
|
|
654
|
+
bag.byRun = byRun;
|
|
655
|
+
} else {
|
|
656
|
+
next = (bag.session ?? 0) + 1;
|
|
657
|
+
bag.session = next;
|
|
658
|
+
}
|
|
659
|
+
writeCountsBag(session, bag);
|
|
660
|
+
return next;
|
|
661
|
+
}
|
|
662
|
+
/** Narrow guard — `Record<string, unknown>` excluding arrays / null. */
|
|
663
|
+
function isPlainObject(v) {
|
|
664
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
665
|
+
}
|
|
666
|
+
function normalizeItem(item) {
|
|
667
|
+
return {
|
|
668
|
+
id: item.id,
|
|
669
|
+
content: item.content,
|
|
670
|
+
status: item.status
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function sanitizeItems(raw, cap) {
|
|
674
|
+
const out = [];
|
|
675
|
+
for (const item of raw) {
|
|
676
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
677
|
+
const obj = item;
|
|
678
|
+
const id = typeof obj.id === "string" ? obj.id : void 0;
|
|
679
|
+
const content = typeof obj.content === "string" ? obj.content : void 0;
|
|
680
|
+
const status = typeof obj.status === "string" && TODO_STATUS_VALUES.includes(obj.status) ? obj.status : void 0;
|
|
681
|
+
if (!id || !content || !status) continue;
|
|
682
|
+
out.push({
|
|
683
|
+
id,
|
|
684
|
+
content,
|
|
685
|
+
status
|
|
686
|
+
});
|
|
687
|
+
if (out.length >= cap) break;
|
|
688
|
+
}
|
|
689
|
+
return out;
|
|
690
|
+
}
|
|
691
|
+
function summarizeStatuses(items) {
|
|
692
|
+
if (items.length === 0) return void 0;
|
|
693
|
+
const counts = {
|
|
694
|
+
pending: 0,
|
|
695
|
+
in_progress: 0,
|
|
696
|
+
completed: 0,
|
|
697
|
+
cancelled: 0
|
|
698
|
+
};
|
|
699
|
+
for (const item of items) counts[item.status] += 1;
|
|
700
|
+
const parts = [];
|
|
701
|
+
if (counts.completed) parts.push(`${counts.completed} completed`);
|
|
702
|
+
if (counts.in_progress) parts.push(`${counts.in_progress} in progress`);
|
|
703
|
+
if (counts.pending) parts.push(`${counts.pending} pending`);
|
|
704
|
+
if (counts.cancelled) parts.push(`${counts.cancelled} cancelled`);
|
|
705
|
+
return parts.length > 0 ? parts.join(" · ") : void 0;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Order-sensitive structural equality on todo lists. Used by the
|
|
709
|
+
* `dedupIdentical` short-circuit to decide whether an incoming payload
|
|
710
|
+
* matches the current run's stored slot.
|
|
711
|
+
*
|
|
712
|
+
* Order-sensitive on purpose: re-ordering the list is a meaningful state
|
|
713
|
+
* change ("the model reprioritized") and should NOT collapse to a no-op.
|
|
714
|
+
* Three-field comparison is exhaustive for `TodoItem` — `normalizeItem`
|
|
715
|
+
* strips any extra fields before they reach the slot, so we never need to
|
|
716
|
+
* compare beyond `{ id, content, status }`.
|
|
717
|
+
*/
|
|
718
|
+
function todosEqual(a, b) {
|
|
719
|
+
if (a.length !== b.length) return false;
|
|
720
|
+
for (let i = 0; i < a.length; i++) {
|
|
721
|
+
const x = a[i];
|
|
722
|
+
const y = b[i];
|
|
723
|
+
if (x.id !== y.id || x.status !== y.status || x.content !== y.content) return false;
|
|
724
|
+
}
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
const EMPTY_TALLY = {
|
|
728
|
+
pending: 0,
|
|
729
|
+
in_progress: 0,
|
|
730
|
+
completed: 0,
|
|
731
|
+
cancelled: 0
|
|
732
|
+
};
|
|
733
|
+
const EMPTY_ACTIVE = {
|
|
734
|
+
runId: null,
|
|
735
|
+
todos: [],
|
|
736
|
+
archive: [],
|
|
737
|
+
inProgress: null,
|
|
738
|
+
tally: EMPTY_TALLY
|
|
739
|
+
};
|
|
740
|
+
/**
|
|
741
|
+
* Pick the "active" run for UI surfaces that show one slot at a time
|
|
742
|
+
* (todos indicator, todos modal, footer ctx indicator). The resolved
|
|
743
|
+
* runId then routes through {@link getTodosForRun} — top-level runs
|
|
744
|
+
* land on the session-shared slot, subagent runs on their own.
|
|
745
|
+
*
|
|
746
|
+
* - A currently-running run wins (latest-started one if several).
|
|
747
|
+
* - Otherwise the most recently appended run wins.
|
|
748
|
+
* - `null` when the session has no runs.
|
|
749
|
+
*
|
|
750
|
+
* Mirrors the "most recent thing that mattered" policy that drives
|
|
751
|
+
* `lastContextSizeFromTurns` — but walks `session.runs` rather than
|
|
752
|
+
* `session.turns` because the active todo slot is a function of the
|
|
753
|
+
* run, not of message history.
|
|
754
|
+
*/
|
|
755
|
+
function pickActiveRunId(session) {
|
|
756
|
+
if (!session) return null;
|
|
757
|
+
const runs = session.runs;
|
|
758
|
+
if (!runs || runs.length === 0) return null;
|
|
759
|
+
let latestRunning = null;
|
|
760
|
+
for (let i = runs.length - 1; i >= 0; i--) if (runs[i].status === "running") {
|
|
761
|
+
latestRunning = runs[i].id;
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
if (latestRunning) return latestRunning;
|
|
765
|
+
return runs[runs.length - 1].id;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Pure selector — derive the active-todos state from a session snapshot.
|
|
769
|
+
* Used by {@link useActiveTodos} and directly testable without React.
|
|
770
|
+
*
|
|
771
|
+
* Resolves both `todos` (live, model-facing) and `archive` (last
|
|
772
|
+
* non-empty snapshot for UI fallback). The tally folds over whichever
|
|
773
|
+
* list is non-empty; `inProgress` is sourced strictly from `todos`
|
|
774
|
+
* because an archived in-progress item is by definition stale.
|
|
775
|
+
*/
|
|
776
|
+
function selectActiveTodos(session) {
|
|
777
|
+
const runId = pickActiveRunId(session);
|
|
778
|
+
if (!session || !runId) return EMPTY_ACTIVE;
|
|
779
|
+
const todos = getTodosForRun(session, runId);
|
|
780
|
+
const archive = getArchivedTodosForRun(session, runId);
|
|
781
|
+
const display = todos.length > 0 ? todos : archive;
|
|
782
|
+
if (display.length === 0) return {
|
|
783
|
+
runId,
|
|
784
|
+
todos,
|
|
785
|
+
archive,
|
|
786
|
+
inProgress: null,
|
|
787
|
+
tally: EMPTY_TALLY
|
|
788
|
+
};
|
|
789
|
+
const tally = {
|
|
790
|
+
pending: 0,
|
|
791
|
+
in_progress: 0,
|
|
792
|
+
completed: 0,
|
|
793
|
+
cancelled: 0
|
|
794
|
+
};
|
|
795
|
+
let inProgress = null;
|
|
796
|
+
for (const t of display) tally[t.status] += 1;
|
|
797
|
+
if (todos.length > 0) {
|
|
798
|
+
for (const t of todos) if (t.status === "in_progress") {
|
|
799
|
+
inProgress = t;
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
runId,
|
|
805
|
+
todos,
|
|
806
|
+
archive,
|
|
807
|
+
inProgress,
|
|
808
|
+
tally
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* React hook — active-todos state for the supplied session. Recomputes
|
|
813
|
+
* on every parent re-render; the work is O(runs + todos), both bounded
|
|
814
|
+
* by small constants in practice.
|
|
815
|
+
*
|
|
816
|
+
* Why no memoization: `Session.runs` is mutated in place by `completeRun`
|
|
817
|
+
* / `abortRun` (status flips without changing array length or item
|
|
818
|
+
* identity), so any memo keyed off length / bag identity would silently
|
|
819
|
+
* return stale state when e.g. a child subagent run completes and the
|
|
820
|
+
* "active" run should reflow to the parent. The selector is cheap
|
|
821
|
+
* enough that running it every parent render is the simpler, correct
|
|
822
|
+
* answer — see `selectActiveTodos` for the O(n) cost.
|
|
823
|
+
*
|
|
824
|
+
* The hook lives in `chat/` so a GUI consumer can mount it verbatim.
|
|
825
|
+
* Renderer-agnostic — no `@opentui/*` imports.
|
|
826
|
+
*/
|
|
827
|
+
function useActiveTodos(session) {
|
|
828
|
+
return selectActiveTodos(session);
|
|
829
|
+
}
|
|
830
|
+
//#endregion
|
|
256
831
|
//#region src/chat/agents.ts
|
|
257
832
|
/**
|
|
258
833
|
* Resolve a profile's `accent` token to a concrete hex color via the
|
|
@@ -314,7 +889,9 @@ const DEFAULT_PERSIST_EXCLUDE_TOOLS = [
|
|
|
314
889
|
"skills_read",
|
|
315
890
|
"present_plan",
|
|
316
891
|
"ask_user",
|
|
317
|
-
"spawn"
|
|
892
|
+
"spawn",
|
|
893
|
+
"todowrite",
|
|
894
|
+
"todoread"
|
|
318
895
|
];
|
|
319
896
|
/**
|
|
320
897
|
* Token-saving `AgentBehavior` defaults shared by the built-in profiles.
|
|
@@ -370,7 +947,7 @@ const BUILD_AGENT = {
|
|
|
370
947
|
label: "Build",
|
|
371
948
|
description: "full tool access — read, write, edit, shell, and spawn subagents",
|
|
372
949
|
accent: "accent",
|
|
373
|
-
preset: definePreset({
|
|
950
|
+
preset: composePresets(definePreset({
|
|
374
951
|
name: "build",
|
|
375
952
|
system: buildBuildSystem(),
|
|
376
953
|
behavior: { ...SHARED_BEHAVIOR },
|
|
@@ -378,7 +955,7 @@ const BUILD_AGENT = {
|
|
|
378
955
|
...BUILD_TOOLS,
|
|
379
956
|
spawn: createSpawnTool({ persist: true })
|
|
380
957
|
}
|
|
381
|
-
})
|
|
958
|
+
}), createTodoTools())
|
|
382
959
|
};
|
|
383
960
|
/**
|
|
384
961
|
* Plan agent — read-only exploration mode. Locked down to file reading and
|
|
@@ -1548,6 +2125,12 @@ const KEYBINDING_DEFS = [
|
|
|
1548
2125
|
label: "effort",
|
|
1549
2126
|
description: "open the reasoning-effort picker (when the active model supports it)"
|
|
1550
2127
|
},
|
|
2128
|
+
{
|
|
2129
|
+
action: "openTodos",
|
|
2130
|
+
default: "ctrl+t",
|
|
2131
|
+
label: "todos",
|
|
2132
|
+
description: "open the active run's todo list (the agent's `todowrite` checkpoints)"
|
|
2133
|
+
},
|
|
1551
2134
|
{
|
|
1552
2135
|
action: "cycleAgent",
|
|
1553
2136
|
default: "shift+tab",
|
|
@@ -2599,10 +3182,6 @@ function eventsFromTurns(turns, runs = []) {
|
|
|
2599
3182
|
pushSpawnStart(run);
|
|
2600
3183
|
openStack.push(run);
|
|
2601
3184
|
}
|
|
2602
|
-
if (depth === 0 && lastDepthAtEmission === 0) events.push({
|
|
2603
|
-
kind: "separator",
|
|
2604
|
-
text: ""
|
|
2605
|
-
});
|
|
2606
3185
|
const subagentTag = depth > 0 && turn.runId ? {
|
|
2607
3186
|
childId: labelFor(turn.runId),
|
|
2608
3187
|
depth
|
|
@@ -2613,11 +3192,17 @@ function eventsFromTurns(turns, runs = []) {
|
|
|
2613
3192
|
};
|
|
2614
3193
|
if (turn.role === "user") {
|
|
2615
3194
|
for (const block of turn.content) if (block.type === "text" && block.text.trim()) {
|
|
2616
|
-
if (depth === 0)
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
3195
|
+
if (depth === 0) {
|
|
3196
|
+
if (lastDepthAtEmission === 0) events.push({
|
|
3197
|
+
kind: "separator",
|
|
3198
|
+
text: ""
|
|
3199
|
+
});
|
|
3200
|
+
events.push({
|
|
3201
|
+
kind: "user-prompt",
|
|
3202
|
+
text: block.text,
|
|
3203
|
+
turnId: turn.id
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
2621
3206
|
} else if (block.type === "tool_result") {
|
|
2622
3207
|
const tool = toolByCallId.get(block.callId);
|
|
2623
3208
|
const raw = toolResultText(block.output);
|
|
@@ -4137,7 +4722,9 @@ const DEFAULT_SETTINGS = {
|
|
|
4137
4722
|
showEditDiffs: true,
|
|
4138
4723
|
targetFps: 60,
|
|
4139
4724
|
allowInteraction: true,
|
|
4140
|
-
smoothStreaming: true
|
|
4725
|
+
smoothStreaming: true,
|
|
4726
|
+
showTodoIndicator: true,
|
|
4727
|
+
showThrobber: false
|
|
4141
4728
|
};
|
|
4142
4729
|
/**
|
|
4143
4730
|
* Hard-clamp a `targetFps` value to a safe range before handing it to
|
|
@@ -4252,6 +4839,16 @@ const SETTINGS_TOGGLES = [
|
|
|
4252
4839
|
key: "smoothStreaming",
|
|
4253
4840
|
label: "Smooth streaming",
|
|
4254
4841
|
description: "drip-feed streamed text character-by-character at a steady cadence (typewriter effect)"
|
|
4842
|
+
},
|
|
4843
|
+
{
|
|
4844
|
+
key: "showTodoIndicator",
|
|
4845
|
+
label: "Todo indicator",
|
|
4846
|
+
description: "show the subtle \"in progress\" todo line above the prompt (modal stays accessible regardless)"
|
|
4847
|
+
},
|
|
4848
|
+
{
|
|
4849
|
+
key: "showThrobber",
|
|
4850
|
+
label: "Streaming throbber",
|
|
4851
|
+
description: "animated gradient glyphs at the transcript tail while the assistant is working"
|
|
4255
4852
|
}
|
|
4256
4853
|
];
|
|
4257
4854
|
const SETTINGS_CHOICES = [
|
|
@@ -5981,7 +6578,7 @@ function getSafelist(dataDir, projectDir) {
|
|
|
5981
6578
|
return readProjects(dataDir)[projectDir]?.safelist ?? [];
|
|
5982
6579
|
}
|
|
5983
6580
|
/**
|
|
5984
|
-
* Tools that always pass without prompting.
|
|
6581
|
+
* Tools that always pass without prompting. Three categories:
|
|
5985
6582
|
*
|
|
5986
6583
|
* - **Pure reads** (`read_file`, `list_files`, `glob`, `grep`) — no
|
|
5987
6584
|
* side effects on disk or the model's own state.
|
|
@@ -5989,6 +6586,12 @@ function getSafelist(dataDir, projectDir) {
|
|
|
5989
6586
|
* itself surfaces a TUI picker the user has to answer, so prompting
|
|
5990
6587
|
* for approval first would mean "approve this prompt before you see
|
|
5991
6588
|
* the prompt": redundant + breaks the flow. The picker IS the gate.
|
|
6589
|
+
* - **Todos** (`todowrite`, `todoread`) — write to a slot on
|
|
6590
|
+
* `session.metadata.todos` and nothing else. No shell, no disk, no
|
|
6591
|
+
* network. The model uses these for checkpointing every few steps;
|
|
6592
|
+
* a per-call approval prompt would drown the conversation in
|
|
6593
|
+
* interruptions that buy no safety since the tool's surface is
|
|
6594
|
+
* read/write on a metadata bag we already trust the agent with.
|
|
5992
6595
|
*
|
|
5993
6596
|
* Users who want to gate any of these must disable safe-mode entirely
|
|
5994
6597
|
* (or fork this list in their own embedding).
|
|
@@ -5999,7 +6602,9 @@ const IMPLICITLY_SAFE_TOOLS = [
|
|
|
5999
6602
|
"glob",
|
|
6000
6603
|
"grep",
|
|
6001
6604
|
"ask_user",
|
|
6002
|
-
"present_plan"
|
|
6605
|
+
"present_plan",
|
|
6606
|
+
"todowrite",
|
|
6607
|
+
"todoread"
|
|
6003
6608
|
];
|
|
6004
6609
|
/** Common input keys carrying the "primary argument" we scope safelists on. */
|
|
6005
6610
|
const PRIMARY_ARG_KEYS = [
|
|
@@ -6515,15 +7120,16 @@ const TICK_INTERVAL_MS = 16;
|
|
|
6515
7120
|
/**
|
|
6516
7121
|
* Smooth-streaming display rate, in characters per second.
|
|
6517
7122
|
*
|
|
6518
|
-
* - {@link SMOOTH_BASE_CPS} —
|
|
6519
|
-
*
|
|
6520
|
-
* typist's pace; comfortably readable without feeling like a deliberate
|
|
7123
|
+
* - {@link SMOOTH_BASE_CPS} — calm-state cap. ~200 CPS is a fast typist's
|
|
7124
|
+
* pace; comfortably readable without feeling like a deliberate
|
|
6521
7125
|
* typewriter performance.
|
|
6522
|
-
* - {@link SMOOTH_BURST_CPS} —
|
|
6523
|
-
*
|
|
6524
|
-
*
|
|
6525
|
-
*
|
|
6526
|
-
* the
|
|
7126
|
+
* - {@link SMOOTH_BURST_CPS} — cap once the buffer is the full
|
|
7127
|
+
* {@link SMOOTH_BURST_BACKLOG_CHARS} behind the provider. The rate
|
|
7128
|
+
* ramps continuously from base to burst across that range (see
|
|
7129
|
+
* {@link smoothCharsForTick}) so the typewriter cadence never visibly
|
|
7130
|
+
* "steps up" mid-stream — the old step function (BASE for <100,
|
|
7131
|
+
* BURST for ≥100) tripled the chars-per-frame the instant the
|
|
7132
|
+
* threshold was crossed, which read as the text jumping.
|
|
6527
7133
|
* - {@link SMOOTH_INSTANT_BACKLOG_CHARS} — failsafe. If the buffer
|
|
6528
7134
|
* somehow accumulates >2K chars (paste-heavy responses, model dumping
|
|
6529
7135
|
* pre-computed text, etc.) the ticker stops smoothing entirely for
|
|
@@ -6532,7 +7138,7 @@ const TICK_INTERVAL_MS = 16;
|
|
|
6532
7138
|
*/
|
|
6533
7139
|
const SMOOTH_BASE_CPS = 200;
|
|
6534
7140
|
const SMOOTH_BURST_CPS = 600;
|
|
6535
|
-
const SMOOTH_BURST_BACKLOG_CHARS =
|
|
7141
|
+
const SMOOTH_BURST_BACKLOG_CHARS = 400;
|
|
6536
7142
|
const SMOOTH_INSTANT_BACKLOG_CHARS = 2e3;
|
|
6537
7143
|
const PARENT_OWNER = "parent";
|
|
6538
7144
|
function emptyBucket(owner, depth) {
|
|
@@ -6656,11 +7262,19 @@ function turnContextSize(usage) {
|
|
|
6656
7262
|
* current backlog of one bucket. Caps at the bucket length (never
|
|
6657
7263
|
* over-drains) and bypasses smoothing entirely once the backlog goes
|
|
6658
7264
|
* past {@link SMOOTH_INSTANT_BACKLOG_CHARS}.
|
|
7265
|
+
*
|
|
7266
|
+
* The CPS ramps continuously from {@link SMOOTH_BASE_CPS} at zero backlog
|
|
7267
|
+
* to {@link SMOOTH_BURST_CPS} at {@link SMOOTH_BURST_BACKLOG_CHARS},
|
|
7268
|
+
* then plateaus at burst up to the instant cliff. Continuous (vs. the
|
|
7269
|
+
* legacy step at the burst threshold) so the chars-per-frame count never
|
|
7270
|
+
* jumps mid-stream — visible jumps in the typewriter cadence read as the
|
|
7271
|
+
* text itself jumping.
|
|
6659
7272
|
*/
|
|
6660
7273
|
function smoothCharsForTick(backlog) {
|
|
6661
7274
|
if (backlog <= 0) return 0;
|
|
6662
7275
|
if (backlog >= SMOOTH_INSTANT_BACKLOG_CHARS) return backlog;
|
|
6663
|
-
|
|
7276
|
+
const cps = SMOOTH_BASE_CPS + Math.min(1, backlog / SMOOTH_BURST_BACKLOG_CHARS) * (SMOOTH_BURST_CPS - SMOOTH_BASE_CPS);
|
|
7277
|
+
return Math.max(1, Math.min(backlog, Math.ceil(cps * TICK_INTERVAL_MS / 1e3)));
|
|
6664
7278
|
}
|
|
6665
7279
|
/**
|
|
6666
7280
|
* Slice `n` chars off the front of `buf` without splitting a UTF-16
|
|
@@ -7044,6 +7658,37 @@ const TOOL_DISPLAY = {
|
|
|
7044
7658
|
};
|
|
7045
7659
|
}
|
|
7046
7660
|
},
|
|
7661
|
+
todowrite: {
|
|
7662
|
+
displayName: "Todos",
|
|
7663
|
+
format: (input) => {
|
|
7664
|
+
const todos = Array.isArray(input.todos) ? input.todos : null;
|
|
7665
|
+
if (!todos) return null;
|
|
7666
|
+
const counts = {
|
|
7667
|
+
pending: 0,
|
|
7668
|
+
in_progress: 0,
|
|
7669
|
+
completed: 0,
|
|
7670
|
+
cancelled: 0
|
|
7671
|
+
};
|
|
7672
|
+
for (const t of todos) {
|
|
7673
|
+
if (!t || typeof t !== "object") continue;
|
|
7674
|
+
const status = t.status;
|
|
7675
|
+
if (typeof status === "string" && status in counts) counts[status] += 1;
|
|
7676
|
+
}
|
|
7677
|
+
const meta = [];
|
|
7678
|
+
if (counts.completed) meta.push(`${counts.completed} done`);
|
|
7679
|
+
if (counts.in_progress) meta.push(`${counts.in_progress} in progress`);
|
|
7680
|
+
if (counts.pending) meta.push(`${counts.pending} pending`);
|
|
7681
|
+
if (counts.cancelled) meta.push(`${counts.cancelled} cancelled`);
|
|
7682
|
+
return {
|
|
7683
|
+
target: `${todos.length} item${todos.length === 1 ? "" : "s"}`,
|
|
7684
|
+
meta
|
|
7685
|
+
};
|
|
7686
|
+
}
|
|
7687
|
+
},
|
|
7688
|
+
todoread: {
|
|
7689
|
+
displayName: "Todos",
|
|
7690
|
+
format: () => ({ target: "read" })
|
|
7691
|
+
},
|
|
7047
7692
|
ask_user: {
|
|
7048
7693
|
displayName: "Ask user",
|
|
7049
7694
|
format: (input) => {
|
|
@@ -7288,6 +7933,6 @@ function countNeighbors(turnIds, turnId) {
|
|
|
7288
7933
|
};
|
|
7289
7934
|
}
|
|
7290
7935
|
//#endregion
|
|
7291
|
-
export { useMcpAuthDispatch as $, bootTick as $n, isVisible as $t, getSafelist as A, KEYBINDING_DEF_BY_ACTION as An,
|
|
7936
|
+
export { useMcpAuthDispatch as $, bootTick as $n, TOKEN_DISCIPLINE_DOCTRINE as $r, isVisible as $t, getSafelist as A, KEYBINDING_DEF_BY_ACTION as An, TODOREAD_TOOL as Ar, clampFps as At, supportsOAuth as B, uniqueSkillNamesFromReferences as Bn, pruneTodosByRun as Br, CATPPUCCIN_MOCHA as Bt, resolveSessionExportTarget as C, maskToOutcomeKinds as Cn, BUILTIN_AGENTS as Cr, shortId as Ct, useSafeModeQueue as D, findGitRoot$1 as Dn, accentColor as Dr, SETTINGS_CHOICES as Dt, useSafeModeActions as E, summarizeOutcomes as En, PLAN_AGENT as Er, DEFAULT_SETTINGS as Et, suggestSafelistEntry as F, parseBindingSpec as Fn, createTodoTools as Fr, resolveTheme as Ft, defaultMcpsConfigPaths as G, collectReferences as Gn, COMMUNICATION_DOCTRINE as Gr, ConfigProvider as Gt, filterModelCatalog as H, createFilesCompletionProvider as Hn, setTodosForRun as Hr, DiscoveryProvider as Ht, writeProjects as I, readKeybindings as In, getArchivedTodosForRun as Ir, VAPORWAVE_THEME as It, projectUserPaths as J, useCompletion as Jn, INTERACTION_GUIDANCE as Jr, createStateStore as Jt, discoverProjectMcps as K, findActiveTrigger as Kn, DOING_TASKS_DOCTRINE as Kr, useConfig as Kt, splitPromptSegments as L, stripJsonComments as Ln, getTodosForRun as Lr, CATPPUCCIN_FRAPPE as Lt, matchesSafelistEntry as M, keybindingsPath as Mn, TODOWRITE_TOOL as Mr, BUILTIN_THEMES as Mt, projectsFilePath as N, matchesBinding as Nn, TODO_STATUS_GLYPHS as Nr, DEFAULT_THEME as Nt, IMPLICITLY_SAFE_TOOLS as O, DEFAULT_KEYBINDINGS as On, resolveAgentId as Or, SETTINGS_TOGGLES as Ot, readProjects as P, mergeKeybindings as Pn, TODO_WRITE_COUNTS_METADATA_KEY as Pr, resolveChipColor as Pt, McpAuthProvider as Q, bootProfileEnabled as Qn, SUBAGENT_GUIDANCE as Qr, isTurnHighlighted as Qt, formatPathForCwd as R, SKILLS_TRIGGER as Rn, isTodoTool as Rr, CATPPUCCIN_LATTE as Rt, renderSession as S, buildEditOutcomesAnnotation as Sn, BUILD_AGENT as Sr, fmtTokens as St, SafeModeProvider as T, resolveApprovalForPayload as Tn, DEFAULT_PERSIST_EXCLUDE_TOOLS as Tr, useEnabledToggleSet as Tt, indexOfEntry as U, uniqueFilesFromReferences as Un, useActiveTodos as Ur, useDiscovery as Ut, buildModelCatalog as V, FILES_TRIGGER as Vn, selectActiveTodos as Vr, createDiscoverySlot as Vt, buildMcpServers as W, applyInsert as Wn, ACTIONS_WITH_CARE_DOCTRINE as Wr, useDiscoveryOptional as Wt, mcpCredentialsPath as X, buildLinearRamp as Xn, PLAN_MODE_DOCTRINE as Xr, eventsFromTurns as Xt, createFileMcpCredentialStore as Y, blendHsl as Yn, INTERACTION_GUIDANCE_NO_PROMPTS as Yr, deriveSessionTitle as Yt, patchMcpCredential as Z, tryOpenBrowser as Zn, PLAN_MODE_DOCTRINE_NO_PROMPTS as Zr, isEditErrorResult as Zt, turnContextSize as _, extractEditPayload as _n, modelSupportsReasoning as _r, truncateTrailing as _t, computeTurnAnchors as a, selectableTurnIds as an, readProviderCredential as ar, InteractionsProvider as at, defaultSkillScanPaths as b, splitLines as bn, openrouterDescriptor as br, ageString as bt, formatToolCall as c, titleFromTurns as cn, writeCredentials as cr, createInteractionTools as ct, useSelectStyle as d, turnSelectionOwnership as dn, anthropicDescriptor as dr, pendingInteractionsFromTurns as dt, buildBuildSystem as ei, lastContextSizeFromTurns as en, shouldAutoCompact as er, useMcpAuthState as et, useSurfaces as f, applyEditPayload as fn, cerebrasDescriptor as fr, serializeInteractionResponse as ft, finalizeStreamingMarkdownForOwner as g, computeLineDiff as gn, getModelInfo as gr, hintsLength as gt, finalizeStreamingMarkdown as h, computeInlineDiff as hn, getContextWindow as hr, clipHintsToWidth as ht, turnAsText as i, saveState as in, readCredentials as ir, ASK_USER_TOOL as it, isOnSafelist as j, ensureKeybindingsFile as jn, TODOS_METADATA_KEY as jr, useSettings as jt, addToSafelist as k, KEYBINDING_DEFS as kn, singleAgentRegistry as kr, SettingsProvider as kt, ThemeProvider as l, toolCallPreview as ln, BUILTIN_PROVIDERS as lr, isInteractionTool as lt, useTheme as m, buildUnifiedDiff as mn, effectiveContextWindow as mr, useInteractionsQueue as mt, deleteTurnSafely as n, envSection as ni, loadState as nn, applyApiKeyEnv as nr, reduceMcpAuth as nt, TOOL_DISPLAY as o, stripSpawnTokensLine as on, removeProviderCredential as or, PRESENT_PLAN_TOOL as ot, useSyntaxStyles as p, buildContextualDiff as pn, credKeyOf as pr, useInteractionsActions as pt, parseMcpsFile as q, mergeReferences as qn, IDENTITY_PREFIX as qr, resolveConfig as qt, truncateTurnsAt as r, marginTopFor as rn, credentialsPath as rr, splitMarkdownCodeBlocks as rt, displayNameFor as s, sumRunCosts as sn, setProviderCredential as sr, buildResumedToolResultsTurn as st, countNeighbors as t, buildPlanSystem as ti, listSessionMeta as tn, detectAuth as tr, getMcpAuthStatus as tt, useColors as u, toolResultText as un, OUTPUT_RESERVE_TOKENS as ur, makeRequestInteraction as ut, useStreamBuffer as v, filetypeFromPath as vn, modelsForDescriptor as vr, cleanTitle as vt, writeSessionExport as w, parseEditOutcomesFromResult as wn, DEFAULT_AGENT_ID as wr, listProjectFiles as wt, discoverProjectSkills as x, tokenize as xn, piIdOf as xr, compactPath as xt, buildSkillsConfig as y, previewEditPayload as yn, openaiDescriptor as yr, generateSessionTitle as yt, runOAuthLogin as z, createSkillsCompletionProvider as zn, pickActiveRunId as zr, CATPPUCCIN_MACCHIATO as zt };
|
|
7292
7937
|
|
|
7293
|
-
//# sourceMappingURL=turn-operations-
|
|
7938
|
+
//# sourceMappingURL=turn-operations-D9HvatsR.js.map
|