@oh-my-pi/pi-coding-agent 14.5.9 → 14.5.11
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/CHANGELOG.md +50 -0
- package/package.json +7 -15
- package/scripts/build-binary.ts +1 -1
- package/src/cli/update-cli.ts +25 -1
- package/src/config/model-registry.ts +21 -19
- package/src/config/settings-schema.ts +11 -16
- package/src/discovery/claude-plugins.ts +28 -3
- package/src/edit/modes/atom.ts +50 -19
- package/src/edit/modes/hashline.ts +171 -110
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +14 -2
- package/src/extensibility/extensions/runner.ts +34 -1
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/internal-urls/docs-index.generated.ts +54 -54
- package/src/lsp/client.ts +27 -35
- package/src/memories/index.ts +5 -0
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/controllers/todo-command-controller.ts +22 -74
- package/src/modes/interactive-mode.ts +36 -9
- package/src/modes/theme/theme.ts +10 -1
- package/src/modes/types.ts +1 -3
- package/src/modes/utils/ui-helpers.ts +19 -6
- package/src/prompts/system/auto-continue.md +1 -0
- package/src/prompts/system/eager-todo.md +1 -1
- package/src/prompts/tools/github.md +3 -3
- package/src/prompts/tools/todo-write.md +19 -19
- package/src/sdk.ts +13 -2
- package/src/session/agent-session.ts +196 -96
- package/src/session/session-manager.ts +19 -2
- package/src/tools/bash.ts +9 -4
- package/src/tools/gh.ts +267 -119
- package/src/tools/todo-write.ts +157 -195
- package/src/utils/git.ts +61 -2
- package/src/web/search/providers/searxng.ts +71 -13
- package/examples/custom-tools/todo/index.ts +0 -211
- package/examples/extensions/todo.ts +0 -295
package/src/lsp/client.ts
CHANGED
|
@@ -47,7 +47,7 @@ function startIdleChecker(): void {
|
|
|
47
47
|
const now = Date.now();
|
|
48
48
|
for (const [key, client] of Array.from(clients.entries())) {
|
|
49
49
|
if (now - client.lastActivity > idleTimeoutMs) {
|
|
50
|
-
shutdownClient(key);
|
|
50
|
+
void shutdownClient(key);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
}, IDLE_CHECK_INTERVAL_MS);
|
|
@@ -762,22 +762,25 @@ export async function refreshFile(client: LspClient, filePath: string, signal?:
|
|
|
762
762
|
/**
|
|
763
763
|
* Shutdown a specific client by key.
|
|
764
764
|
*/
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
if (!client) return;
|
|
768
|
-
|
|
769
|
-
// Reject all pending requests
|
|
765
|
+
async function shutdownClientInstance(client: LspClient): Promise<void> {
|
|
766
|
+
const err = new Error("LSP client shutdown");
|
|
770
767
|
for (const pending of Array.from(client.pendingRequests.values())) {
|
|
771
|
-
pending.reject(
|
|
768
|
+
pending.reject(err);
|
|
772
769
|
}
|
|
773
770
|
client.pendingRequests.clear();
|
|
774
771
|
|
|
775
|
-
|
|
776
|
-
sendRequest(client, "shutdown", null).catch(() => {});
|
|
777
|
-
|
|
778
|
-
// Kill process
|
|
772
|
+
const timeout = Bun.sleep(5_000);
|
|
773
|
+
const shutdown = sendRequest(client, "shutdown", null).catch(() => {});
|
|
774
|
+
await Promise.race([shutdown, timeout]);
|
|
779
775
|
client.proc.kill();
|
|
776
|
+
await Promise.race([client.proc.exited.catch(() => {}), Bun.sleep(1_000)]);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export async function shutdownClient(key: string): Promise<void> {
|
|
780
|
+
const client = clients.get(key);
|
|
781
|
+
if (!client) return;
|
|
780
782
|
clients.delete(key);
|
|
783
|
+
await shutdownClientInstance(client);
|
|
781
784
|
}
|
|
782
785
|
|
|
783
786
|
// =============================================================================
|
|
@@ -890,27 +893,10 @@ export async function sendNotification(client: LspClient, method: string, params
|
|
|
890
893
|
/**
|
|
891
894
|
* Shutdown all LSP clients.
|
|
892
895
|
*/
|
|
893
|
-
export function shutdownAll(): void {
|
|
896
|
+
export async function shutdownAll(): Promise<void> {
|
|
894
897
|
const clientsToShutdown = Array.from(clients.values());
|
|
895
898
|
clients.clear();
|
|
896
|
-
|
|
897
|
-
const err = new Error("LSP client shutdown");
|
|
898
|
-
for (const client of clientsToShutdown) {
|
|
899
|
-
/// Reject all pending requests
|
|
900
|
-
const reqs = Array.from(client.pendingRequests.values());
|
|
901
|
-
client.pendingRequests.clear();
|
|
902
|
-
for (const pending of reqs) {
|
|
903
|
-
pending.reject(err);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
void (async () => {
|
|
907
|
-
// Send shutdown request (best effort, don't wait)
|
|
908
|
-
const timeout = Bun.sleep(5_000);
|
|
909
|
-
const result = sendRequest(client, "shutdown", null).catch(() => {});
|
|
910
|
-
await Promise.race([result, timeout]);
|
|
911
|
-
client.proc.kill();
|
|
912
|
-
})().catch(() => {});
|
|
913
|
-
}
|
|
899
|
+
await Promise.allSettled(clientsToShutdown.map(client => shutdownClientInstance(client)));
|
|
914
900
|
}
|
|
915
901
|
|
|
916
902
|
/** Status of an LSP server */
|
|
@@ -938,13 +924,19 @@ export function getActiveClients(): LspServerStatus[] {
|
|
|
938
924
|
|
|
939
925
|
// Register cleanup on module unload
|
|
940
926
|
if (typeof process !== "undefined") {
|
|
941
|
-
process.on("beforeExit",
|
|
927
|
+
process.on("beforeExit", () => {
|
|
928
|
+
void shutdownAll();
|
|
929
|
+
});
|
|
942
930
|
process.on("SIGINT", () => {
|
|
943
|
-
|
|
944
|
-
|
|
931
|
+
void (async () => {
|
|
932
|
+
await shutdownAll();
|
|
933
|
+
process.exit(0);
|
|
934
|
+
})();
|
|
945
935
|
});
|
|
946
936
|
process.on("SIGTERM", () => {
|
|
947
|
-
|
|
948
|
-
|
|
937
|
+
void (async () => {
|
|
938
|
+
await shutdownAll();
|
|
939
|
+
process.exit(0);
|
|
940
|
+
})();
|
|
949
941
|
});
|
|
950
942
|
}
|
package/src/memories/index.ts
CHANGED
|
@@ -277,6 +277,11 @@ async function runPhase1(options: {
|
|
|
277
277
|
});
|
|
278
278
|
|
|
279
279
|
if (result.kind === "failed") {
|
|
280
|
+
logger.error("Memory phase1 stage1 job failed", {
|
|
281
|
+
threadId: claim.threadId,
|
|
282
|
+
rolloutPath: claim.rolloutPath,
|
|
283
|
+
reason: result.reason,
|
|
284
|
+
});
|
|
280
285
|
markStage1Failed(db, {
|
|
281
286
|
threadId: claim.threadId,
|
|
282
287
|
ownershipToken: claim.ownershipToken,
|
|
@@ -348,7 +348,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
348
348
|
{ value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },
|
|
349
349
|
{ value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
|
|
350
350
|
{ value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
|
|
351
|
-
{ value: "searxng", label: "SearXNG", description: "
|
|
351
|
+
{ value: "searxng", label: "SearXNG", description: "Requires searxng.endpoint" },
|
|
352
352
|
],
|
|
353
353
|
"providers.image": [
|
|
354
354
|
{
|
|
@@ -659,9 +659,9 @@ export class SelectorController {
|
|
|
659
659
|
return;
|
|
660
660
|
}
|
|
661
661
|
|
|
662
|
-
// Update UI
|
|
662
|
+
// Update UI — pass the context built by navigateTree to skip a second O(N) walk.
|
|
663
663
|
this.ctx.chatContainer.clear();
|
|
664
|
-
this.ctx.renderInitialMessages();
|
|
664
|
+
this.ctx.renderInitialMessages(result.sessionContext);
|
|
665
665
|
await this.ctx.reloadTodos();
|
|
666
666
|
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
667
667
|
this.ctx.editor.setText(result.editorText);
|
|
@@ -21,7 +21,7 @@ const USAGE = [
|
|
|
21
21
|
" /todo export <path> Write todos as Markdown to <path>",
|
|
22
22
|
" /todo import <path> Replace todos from Markdown at <path>",
|
|
23
23
|
" /todo append [<phase>] <task...> Append a task; phase fuzzy-matched or auto-created",
|
|
24
|
-
" /todo start <task> Mark task in_progress (
|
|
24
|
+
" /todo start <task> Mark task in_progress (fuzzy content match)",
|
|
25
25
|
" /todo done [<task|phase>] Mark task/phase/all completed",
|
|
26
26
|
" /todo drop [<task|phase>] Mark task/phase/all abandoned",
|
|
27
27
|
" /todo rm [<task|phase>] Remove task/phase/all",
|
|
@@ -59,44 +59,9 @@ function tokenize(input: string): string[] {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// =============================================================================
|
|
62
|
-
//
|
|
62
|
+
// Name normalization
|
|
63
63
|
// =============================================================================
|
|
64
64
|
|
|
65
|
-
const ROMAN_PAIRS: Array<[number, string]> = [
|
|
66
|
-
[1000, "M"],
|
|
67
|
-
[900, "CM"],
|
|
68
|
-
[500, "D"],
|
|
69
|
-
[400, "CD"],
|
|
70
|
-
[100, "C"],
|
|
71
|
-
[90, "XC"],
|
|
72
|
-
[50, "L"],
|
|
73
|
-
[40, "XL"],
|
|
74
|
-
[10, "X"],
|
|
75
|
-
[9, "IX"],
|
|
76
|
-
[5, "V"],
|
|
77
|
-
[4, "IV"],
|
|
78
|
-
[1, "I"],
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
function toRoman(n: number): string {
|
|
82
|
-
if (n <= 0) return "I";
|
|
83
|
-
let out = "";
|
|
84
|
-
let rem = n;
|
|
85
|
-
for (const [value, sym] of ROMAN_PAIRS) {
|
|
86
|
-
while (rem >= value) {
|
|
87
|
-
out += sym;
|
|
88
|
-
rem -= value;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return out;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const PHASE_PREFIX_RE = /^([IVXLCDM]+|[A-Z]|\d+)\.\s*/i;
|
|
95
|
-
|
|
96
|
-
function stripPrefix(name: string): string {
|
|
97
|
-
return name.replace(PHASE_PREFIX_RE, "").trim();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
65
|
function titleCase(s: string): string {
|
|
101
66
|
return s
|
|
102
67
|
.split(/\s+/)
|
|
@@ -105,13 +70,6 @@ function titleCase(s: string): string {
|
|
|
105
70
|
.join(" ");
|
|
106
71
|
}
|
|
107
72
|
|
|
108
|
-
function buildPhaseName(rawName: string, existingPhases: TodoPhase[]): string {
|
|
109
|
-
const stripped = stripPrefix(rawName.trim());
|
|
110
|
-
if (!stripped) return `${toRoman(existingPhases.length + 1)}. Todos`;
|
|
111
|
-
const titled = titleCase(stripped);
|
|
112
|
-
return `${toRoman(existingPhases.length + 1)}. ${titled}`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
73
|
// =============================================================================
|
|
116
74
|
// Fuzzy matching
|
|
117
75
|
// =============================================================================
|
|
@@ -119,20 +77,13 @@ function buildPhaseName(rawName: string, existingPhases: TodoPhase[]): string {
|
|
|
119
77
|
function findPhaseFuzzy(phases: TodoPhase[], query: string): TodoPhase | undefined {
|
|
120
78
|
const q = query.trim().toLowerCase();
|
|
121
79
|
if (!q) return undefined;
|
|
122
|
-
// Exact id
|
|
123
|
-
const byId = phases.find(p => p.id.toLowerCase() === q);
|
|
124
|
-
if (byId) return byId;
|
|
125
80
|
// Exact name (case-insensitive)
|
|
126
81
|
const byName = phases.find(p => p.name.toLowerCase() === q);
|
|
127
82
|
if (byName) return byName;
|
|
128
|
-
//
|
|
129
|
-
const
|
|
130
|
-
const byStripped = phases.find(p => stripPrefix(p.name).toLowerCase() === strippedQ);
|
|
131
|
-
if (byStripped) return byStripped;
|
|
132
|
-
// Substring (prefer prefix match on stripped name)
|
|
133
|
-
const prefixMatches = phases.filter(p => stripPrefix(p.name).toLowerCase().startsWith(strippedQ));
|
|
83
|
+
// Substring (prefer prefix match)
|
|
84
|
+
const prefixMatches = phases.filter(p => p.name.toLowerCase().startsWith(q));
|
|
134
85
|
if (prefixMatches.length === 1) return prefixMatches[0];
|
|
135
|
-
const subMatches = phases.filter(p =>
|
|
86
|
+
const subMatches = phases.filter(p => p.name.toLowerCase().includes(q));
|
|
136
87
|
if (subMatches.length === 1) return subMatches[0];
|
|
137
88
|
return undefined;
|
|
138
89
|
}
|
|
@@ -140,9 +91,10 @@ function findPhaseFuzzy(phases: TodoPhase[], query: string): TodoPhase | undefin
|
|
|
140
91
|
function findTaskFuzzy(phases: TodoPhase[], query: string): { task: TodoItem; phase: TodoPhase } | undefined {
|
|
141
92
|
const q = query.trim().toLowerCase();
|
|
142
93
|
if (!q) return undefined;
|
|
94
|
+
// Exact content (case-insensitive)
|
|
143
95
|
for (const phase of phases) {
|
|
144
96
|
for (const task of phase.tasks) {
|
|
145
|
-
if (task.
|
|
97
|
+
if (task.content.toLowerCase() === q) return { task, phase };
|
|
146
98
|
}
|
|
147
99
|
}
|
|
148
100
|
const matches: Array<{ task: TodoItem; phase: TodoPhase }> = [];
|
|
@@ -169,7 +121,7 @@ function buildSystemReminder(action: string, phases: TodoPhase[]): string {
|
|
|
169
121
|
return [
|
|
170
122
|
"<system-reminder>",
|
|
171
123
|
`The user manually modified the todo list (${action}).`,
|
|
172
|
-
"Current todo list
|
|
124
|
+
"Current todo list:",
|
|
173
125
|
"",
|
|
174
126
|
md,
|
|
175
127
|
"</system-reminder>",
|
|
@@ -327,28 +279,24 @@ export class TodoCommandController {
|
|
|
327
279
|
if (phaseName) {
|
|
328
280
|
targetPhase = findPhaseFuzzy(next, phaseName);
|
|
329
281
|
if (!targetPhase) {
|
|
330
|
-
|
|
331
|
-
targetPhase = { id: `phase-${next.length + 1}`, name: newName, tasks: [] };
|
|
282
|
+
targetPhase = { name: titleCase(phaseName), tasks: [] };
|
|
332
283
|
next.push(targetPhase);
|
|
333
284
|
}
|
|
334
285
|
} else if (next.length > 0) {
|
|
335
286
|
targetPhase = next[next.length - 1];
|
|
336
287
|
} else {
|
|
337
|
-
targetPhase = {
|
|
288
|
+
targetPhase = { name: "Todos", tasks: [] };
|
|
338
289
|
next.push(targetPhase);
|
|
339
290
|
}
|
|
340
291
|
|
|
341
|
-
const
|
|
342
|
-
let n = 1;
|
|
343
|
-
while (usedTaskIds.has(`task-${n}`)) n++;
|
|
292
|
+
const finalContent = titleCaseSentence(content);
|
|
344
293
|
targetPhase.tasks.push({
|
|
345
|
-
|
|
346
|
-
content: titleCaseSentence(content),
|
|
294
|
+
content: finalContent,
|
|
347
295
|
status: "pending",
|
|
348
296
|
});
|
|
349
297
|
|
|
350
298
|
this.#commit(next, `/todo append → ${targetPhase.name}`);
|
|
351
|
-
this.ctx.showStatus(`Appended to ${targetPhase.name}: ${
|
|
299
|
+
this.ctx.showStatus(`Appended to ${targetPhase.name}: ${finalContent}`);
|
|
352
300
|
}
|
|
353
301
|
|
|
354
302
|
// ------------------------------------------------------------- start / done / drop / rm
|
|
@@ -364,12 +312,12 @@ export class TodoCommandController {
|
|
|
364
312
|
this.ctx.showError(`No task matched "${rest}". Use /todo to list current tasks.`);
|
|
365
313
|
return;
|
|
366
314
|
}
|
|
367
|
-
const { phases, errors } = applyOpsToPhases(current, [{ op: "start", task: hit.task.
|
|
315
|
+
const { phases, errors } = applyOpsToPhases(current, [{ op: "start", task: hit.task.content }]);
|
|
368
316
|
if (errors.length > 0) {
|
|
369
317
|
this.ctx.showError(errors.join("; "));
|
|
370
318
|
return;
|
|
371
319
|
}
|
|
372
|
-
this.#commit(phases, `/todo start ${hit.task.
|
|
320
|
+
this.#commit(phases, `/todo start ${hit.task.content}`);
|
|
373
321
|
this.ctx.showStatus(`Started: ${hit.task.content}`);
|
|
374
322
|
}
|
|
375
323
|
|
|
@@ -391,19 +339,19 @@ export class TodoCommandController {
|
|
|
391
339
|
|
|
392
340
|
const taskHit = findTaskFuzzy(current, trimmed);
|
|
393
341
|
if (taskHit) {
|
|
394
|
-
const { phases, errors } = applyOpsToPhases(current, [{ op, task: taskHit.task.
|
|
342
|
+
const { phases, errors } = applyOpsToPhases(current, [{ op, task: taskHit.task.content }]);
|
|
395
343
|
if (errors.length > 0) {
|
|
396
344
|
this.ctx.showError(errors.join("; "));
|
|
397
345
|
return;
|
|
398
346
|
}
|
|
399
|
-
this.#commit(phases, `/todo ${op} ${taskHit.task.
|
|
347
|
+
this.#commit(phases, `/todo ${op} ${taskHit.task.content}`);
|
|
400
348
|
this.ctx.showStatus(`Marked ${target}: ${taskHit.task.content}`);
|
|
401
349
|
return;
|
|
402
350
|
}
|
|
403
351
|
|
|
404
352
|
const phaseHit = findPhaseFuzzy(current, trimmed);
|
|
405
353
|
if (phaseHit) {
|
|
406
|
-
const { phases, errors } = applyOpsToPhases(current, [{ op, phase: phaseHit.
|
|
354
|
+
const { phases, errors } = applyOpsToPhases(current, [{ op, phase: phaseHit.name }]);
|
|
407
355
|
if (errors.length > 0) {
|
|
408
356
|
this.ctx.showError(errors.join("; "));
|
|
409
357
|
return;
|
|
@@ -426,18 +374,18 @@ export class TodoCommandController {
|
|
|
426
374
|
}
|
|
427
375
|
const taskHit = findTaskFuzzy(current, trimmed);
|
|
428
376
|
if (taskHit) {
|
|
429
|
-
const { phases, errors } = applyOpsToPhases(current, [{ op: "rm", task: taskHit.task.
|
|
377
|
+
const { phases, errors } = applyOpsToPhases(current, [{ op: "rm", task: taskHit.task.content }]);
|
|
430
378
|
if (errors.length > 0) {
|
|
431
379
|
this.ctx.showError(errors.join("; "));
|
|
432
380
|
return;
|
|
433
381
|
}
|
|
434
|
-
this.#commit(phases, `/todo rm ${taskHit.task.
|
|
382
|
+
this.#commit(phases, `/todo rm ${taskHit.task.content}`);
|
|
435
383
|
this.ctx.showStatus(`Removed: ${taskHit.task.content}`);
|
|
436
384
|
return;
|
|
437
385
|
}
|
|
438
386
|
const phaseHit = findPhaseFuzzy(current, trimmed);
|
|
439
387
|
if (phaseHit) {
|
|
440
|
-
const { phases, errors } = applyOpsToPhases(current, [{ op: "rm", phase: phaseHit.
|
|
388
|
+
const { phases, errors } = applyOpsToPhases(current, [{ op: "rm", phase: phaseHit.name }]);
|
|
441
389
|
if (errors.length > 0) {
|
|
442
390
|
this.ctx.showError(errors.join("; "));
|
|
443
391
|
return;
|
|
@@ -460,7 +408,7 @@ export class TodoCommandController {
|
|
|
460
408
|
|
|
461
409
|
const current = this.#currentPhases();
|
|
462
410
|
const initialMarkdown =
|
|
463
|
-
current.length > 0 ? phasesToMarkdown(current) : "#
|
|
411
|
+
current.length > 0 ? phasesToMarkdown(current) : "# Todos\n- [ ] (replace this with your tasks)\n";
|
|
464
412
|
|
|
465
413
|
const fileHandle = await this.#openTtyHandle();
|
|
466
414
|
this.ctx.ui.stop();
|
|
@@ -14,7 +14,17 @@ import {
|
|
|
14
14
|
type UsageReport,
|
|
15
15
|
} from "@oh-my-pi/pi-ai";
|
|
16
16
|
import type { Component, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
Container,
|
|
19
|
+
clearRenderCache,
|
|
20
|
+
Loader,
|
|
21
|
+
Markdown,
|
|
22
|
+
ProcessTerminal,
|
|
23
|
+
Spacer,
|
|
24
|
+
Text,
|
|
25
|
+
TUI,
|
|
26
|
+
visibleWidth,
|
|
27
|
+
} from "@oh-my-pi/pi-tui";
|
|
18
28
|
import { APP_NAME, getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@oh-my-pi/pi-utils";
|
|
19
29
|
import chalk from "chalk";
|
|
20
30
|
import { KeybindingsManager } from "../config/keybindings";
|
|
@@ -38,6 +48,7 @@ import { getRecentSessions } from "../session/session-manager";
|
|
|
38
48
|
import { STTController, type SttState } from "../stt";
|
|
39
49
|
import type { ExitPlanModeDetails, LspStartupServerInfo } from "../tools";
|
|
40
50
|
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
51
|
+
import { formatPhaseDisplayName } from "../tools/todo-write";
|
|
41
52
|
import type { EventBus } from "../utils/event-bus";
|
|
42
53
|
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
|
43
54
|
import { getSessionAccentAnsi, getSessionAccentHexForTitle } from "../utils/session-color";
|
|
@@ -442,6 +453,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
442
453
|
|
|
443
454
|
// Set up theme file watcher
|
|
444
455
|
onThemeChange(() => {
|
|
456
|
+
clearRenderCache();
|
|
445
457
|
this.ui.invalidate();
|
|
446
458
|
this.updateEditorBorderColor();
|
|
447
459
|
this.ui.requestRender();
|
|
@@ -696,9 +708,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
696
708
|
const lines = ["", indent + theme.bold(theme.fg("accent", "Todos"))];
|
|
697
709
|
|
|
698
710
|
if (!this.todoExpanded) {
|
|
699
|
-
const
|
|
711
|
+
const activeIdx = phases.indexOf(this.#getActivePhase(phases) ?? phases[0]);
|
|
712
|
+
const activePhase = phases[activeIdx];
|
|
700
713
|
if (!activePhase) return;
|
|
701
|
-
lines.push(
|
|
714
|
+
lines.push(
|
|
715
|
+
`${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(activePhase.name, activeIdx + 1)}`)}`,
|
|
716
|
+
);
|
|
702
717
|
const visibleTasks = activePhase.tasks.slice(0, 5);
|
|
703
718
|
visibleTasks.forEach((todo, index) => {
|
|
704
719
|
const prefix = `${indent}${index === 0 ? hook : " "} `;
|
|
@@ -712,13 +727,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
712
727
|
return;
|
|
713
728
|
}
|
|
714
729
|
|
|
715
|
-
|
|
716
|
-
lines.push(`${indent}${theme.fg("accent", `${hook} ${phase.name}`)}`);
|
|
730
|
+
phases.forEach((phase, phaseIndex) => {
|
|
731
|
+
lines.push(`${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(phase.name, phaseIndex + 1)}`)}`);
|
|
717
732
|
phase.tasks.forEach((todo, index) => {
|
|
718
733
|
const prefix = `${indent}${index === 0 ? hook : " "} `;
|
|
719
734
|
lines.push(this.#formatTodoLine(todo, prefix));
|
|
720
735
|
});
|
|
721
|
-
}
|
|
736
|
+
});
|
|
722
737
|
|
|
723
738
|
this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
|
|
724
739
|
}
|
|
@@ -867,6 +882,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
867
882
|
} else {
|
|
868
883
|
await this.session.setModelTemporary(prev.model, prev.thinkingLevel);
|
|
869
884
|
}
|
|
885
|
+
// If #applyPlanModeModel queued a deferred switch to the plan-role model
|
|
886
|
+
// (because the session was streaming on entry), drop it now: we are
|
|
887
|
+
// leaving plan mode, so flushing it on the next agent_end would land the
|
|
888
|
+
// session on the plan-role model after the user has exited plan mode
|
|
889
|
+
// (issue #816). Only clear when the pending target matches the plan-role
|
|
890
|
+
// model — leave any unrelated user-queued switch intact.
|
|
891
|
+
const pending = this.#pendingModelSwitch;
|
|
892
|
+
if (pending) {
|
|
893
|
+
const planResolution = this.session.resolveRoleModelWithThinking("plan");
|
|
894
|
+
if (planResolution.model && modelsAreEqual(pending.model, planResolution.model)) {
|
|
895
|
+
this.#pendingModelSwitch = undefined;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
870
898
|
}
|
|
871
899
|
this.session.setPlanModeState(undefined);
|
|
872
900
|
this.planModeEnabled = false;
|
|
@@ -1334,8 +1362,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1334
1362
|
this.#uiHelpers.renderSessionContext(sessionContext, options);
|
|
1335
1363
|
}
|
|
1336
1364
|
|
|
1337
|
-
renderInitialMessages(): void {
|
|
1338
|
-
this.#uiHelpers.renderInitialMessages();
|
|
1365
|
+
renderInitialMessages(prebuiltContext?: SessionContext): void {
|
|
1366
|
+
this.#uiHelpers.renderInitialMessages(prebuiltContext);
|
|
1339
1367
|
}
|
|
1340
1368
|
|
|
1341
1369
|
getUserMessageText(message: Message): string {
|
|
@@ -1688,7 +1716,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1688
1716
|
} else {
|
|
1689
1717
|
this.todoPhases = [
|
|
1690
1718
|
{
|
|
1691
|
-
id: "default",
|
|
1692
1719
|
name: "Todos",
|
|
1693
1720
|
tasks: todos as TodoItem[],
|
|
1694
1721
|
},
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -2328,8 +2328,14 @@ export function getSymbolTheme(): SymbolTheme {
|
|
|
2328
2328
|
};
|
|
2329
2329
|
}
|
|
2330
2330
|
|
|
2331
|
+
let _markdownTheme: MarkdownTheme | undefined;
|
|
2332
|
+
let _markdownThemeRef: Theme | undefined;
|
|
2333
|
+
|
|
2331
2334
|
export function getMarkdownTheme(): MarkdownTheme {
|
|
2332
|
-
|
|
2335
|
+
if (_markdownTheme !== undefined && _markdownThemeRef === theme) {
|
|
2336
|
+
return _markdownTheme;
|
|
2337
|
+
}
|
|
2338
|
+
const markdownTheme: MarkdownTheme = {
|
|
2333
2339
|
heading: (text: string) => theme.fg("mdHeading", text),
|
|
2334
2340
|
link: (text: string) => theme.fg("mdLink", text),
|
|
2335
2341
|
linkUrl: (text: string) => theme.fg("mdLinkUrl", text),
|
|
@@ -2355,6 +2361,9 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|
|
2355
2361
|
}
|
|
2356
2362
|
},
|
|
2357
2363
|
};
|
|
2364
|
+
_markdownTheme = markdownTheme;
|
|
2365
|
+
_markdownThemeRef = theme;
|
|
2366
|
+
return markdownTheme;
|
|
2358
2367
|
}
|
|
2359
2368
|
|
|
2360
2369
|
export function getSelectListTheme(): SelectListTheme {
|
package/src/modes/types.ts
CHANGED
|
@@ -42,7 +42,6 @@ export type SubmittedUserInput = {
|
|
|
42
42
|
export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned";
|
|
43
43
|
|
|
44
44
|
export type TodoItem = {
|
|
45
|
-
id: string;
|
|
46
45
|
content: string;
|
|
47
46
|
status: TodoStatus;
|
|
48
47
|
details?: string;
|
|
@@ -50,7 +49,6 @@ export type TodoItem = {
|
|
|
50
49
|
};
|
|
51
50
|
|
|
52
51
|
export type TodoPhase = {
|
|
53
|
-
id: string;
|
|
54
52
|
name: string;
|
|
55
53
|
tasks: TodoItem[];
|
|
56
54
|
};
|
|
@@ -159,7 +157,7 @@ export interface InteractiveModeContext {
|
|
|
159
157
|
sessionContext: SessionContext,
|
|
160
158
|
options?: { updateFooter?: boolean; populateHistory?: boolean },
|
|
161
159
|
): void;
|
|
162
|
-
renderInitialMessages(): void;
|
|
160
|
+
renderInitialMessages(prebuiltContext?: SessionContext): void;
|
|
163
161
|
getUserMessageText(message: Message): string;
|
|
164
162
|
findLastAssistantMessage(): AssistantMessage | undefined;
|
|
165
163
|
extractAssistantText(message: AssistantMessage): string;
|
|
@@ -414,7 +414,7 @@ export class UiHelpers {
|
|
|
414
414
|
this.ctx.ui.requestRender();
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
renderInitialMessages(): void {
|
|
417
|
+
renderInitialMessages(prebuiltContext?: SessionContext): void {
|
|
418
418
|
// This path is used to rebuild the visible chat transcript (e.g. after custom/debug UI).
|
|
419
419
|
// Clear existing rendered chat first to avoid duplicating the full session in the container.
|
|
420
420
|
this.ctx.chatContainer.clear();
|
|
@@ -422,8 +422,8 @@ export class UiHelpers {
|
|
|
422
422
|
this.ctx.pendingBashComponents = [];
|
|
423
423
|
this.ctx.pendingPythonComponents = [];
|
|
424
424
|
|
|
425
|
-
//
|
|
426
|
-
const context = this.ctx.sessionManager.buildSessionContext();
|
|
425
|
+
// Reuse a pre-built context when available (e.g. from navigateTree) to avoid a second O(N) walk.
|
|
426
|
+
const context = prebuiltContext ?? this.ctx.sessionManager.buildSessionContext();
|
|
427
427
|
this.ctx.renderSessionContext(context, {
|
|
428
428
|
updateFooter: true,
|
|
429
429
|
populateHistory: true,
|
|
@@ -610,9 +610,22 @@ export class UiHelpers {
|
|
|
610
610
|
await this.ctx.session.prompt(message.text);
|
|
611
611
|
}
|
|
612
612
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
613
|
+
// Pass streamingBehavior so that if the session is still streaming when
|
|
614
|
+
// compaction-end fires (race window between isStreaming flipping false and
|
|
615
|
+
// the event landing here), prompt() routes the message into the steer/
|
|
616
|
+
// follow-up queue instead of throwing AgentBusyError. When the session is
|
|
617
|
+
// genuinely idle, streamingBehavior is ignored and a fresh prompt runs as
|
|
618
|
+
// before. This keeps the steer preview honest: if delivery has to be
|
|
619
|
+
// deferred, the message lands in the same queue every other consumer
|
|
620
|
+
// (Alt+Up dequeue, post-stream drain) already drains, instead of being
|
|
621
|
+
// stranded in compactionQueuedMessages with no drainer.
|
|
622
|
+
const promptPromise = this.ctx.session
|
|
623
|
+
.prompt(firstPrompt.text, {
|
|
624
|
+
streamingBehavior: firstPrompt.mode === "followUp" ? "followUp" : "steer",
|
|
625
|
+
})
|
|
626
|
+
.catch((error: unknown) => {
|
|
627
|
+
restoreQueue(error);
|
|
628
|
+
});
|
|
616
629
|
|
|
617
630
|
for (const message of rest) {
|
|
618
631
|
if (this.ctx.isKnownSlashCommand(message.text)) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Resume work on the user's most recent intent. Re-read the kept recent messages above the summary to confirm what the user asked for last; if their latest request supersedes earlier plans recorded in the summary, follow the latest request. If there is nothing left to do, say so briefly instead of inventing further work.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Before doing substantive work on the upcoming user request, create a comprehensive phased todo first.
|
|
3
3
|
|
|
4
4
|
You **MUST** call `todo_write` first in this turn.
|
|
5
|
-
You **MUST** initialize the todo list with a single `
|
|
5
|
+
You **MUST** initialize the todo list with a single `init` op.
|
|
6
6
|
You **MUST** cover the entire request from investigation through implementation and verification — not just the next immediate step.
|
|
7
7
|
You **MUST** make task descriptions specific enough that a future turn can execute them without re-planning.
|
|
8
8
|
You **MUST** keep task `content` to a short label (5-10 words). Put file paths, implementation steps, and specifics in `details`.
|
|
@@ -4,9 +4,9 @@ GitHub CLI tool with a single op-based dispatch. Wraps `gh` for repository, issu
|
|
|
4
4
|
Pick the operation via `op`. Each op uses a subset of the parameters:
|
|
5
5
|
- `repo_view` — Read repository metadata. Optional `repo` (owner/repo) and `branch`. Falls back to the current checkout or default `gh` repo.
|
|
6
6
|
- `issue_view` — Read an issue. Required `issue` (number or URL). Optional `repo`. Set `comments: false` to skip discussion.
|
|
7
|
-
- `pr_view` — Read
|
|
8
|
-
- `pr_diff` — Read
|
|
9
|
-
- `pr_checkout` — Check
|
|
7
|
+
- `pr_view` — Read one or more pull requests, including reviews and inline review comments. Optional `pr` (number, URL, branch, or array of any — pass an array to fetch multiple PRs in one call); omitting it targets the current branch's PR. Optional `repo`. Set `comments: false` for a lighter summary.
|
|
8
|
+
- `pr_diff` — Read one or more pull request diffs. Optional `pr` (single identifier or array for batch). Optional `repo`. Set `nameOnly: true` for changed file names. Use `exclude` to drop generated paths from the diff.
|
|
9
|
+
- `pr_checkout` — Check one or more pull requests out into dedicated git worktrees. Optional `pr` (number, URL, branch, or array of any of those — pass an array to batch-check-out multiple PRs in one call), `repo`, `force` (reset existing local branch).
|
|
10
10
|
- `pr_push` — Push a checked-out PR branch back to its source branch. Requires the branch to have been checked out via `op: pr_checkout` (carries push metadata). Optional `branch`; defaults to the current checked-out git branch. Optional `forceWithLease`.
|
|
11
11
|
- `search_issues` — Search issues using normal GitHub issue search syntax. Required `query`. Optional `repo`, `limit`.
|
|
12
12
|
- `search_prs` — Search pull requests using normal GitHub PR search syntax. Required `query`. Optional `repo`, `limit`.
|
|
@@ -5,23 +5,23 @@ The next pending task is auto-promoted to `in_progress` after each completion.
|
|
|
5
5
|
|
|
6
6
|
|`op`|Required fields|Effect|
|
|
7
7
|
|---|---|---|
|
|
8
|
-
|`
|
|
9
|
-
|`start`|`task`|
|
|
10
|
-
|`done`|`task` or `phase
|
|
11
|
-
|`drop`|`task` or `phase
|
|
12
|
-
|`rm`|`task` or `phase
|
|
13
|
-
|`append`|`phase`, `items:
|
|
14
|
-
|`note`|`task`, `text`|Append a note to
|
|
8
|
+
|`init`|`list`|Initialize the full list|
|
|
9
|
+
|`start`|`task`|Mark in progress|
|
|
10
|
+
|`done`|`task` or `phase`|Mark completed|
|
|
11
|
+
|`drop`|`task` or `phase`|Mark abandoned|
|
|
12
|
+
|`rm`|`task` or `phase`|Remove|
|
|
13
|
+
|`append`|`phase`, `items: string[]`|Append tasks; lazily creates phase|
|
|
14
|
+
|`note`|`task`, `text`|Append a note to a task. Reminders for future-you only.|
|
|
15
15
|
|
|
16
16
|
## Anatomy
|
|
17
|
-
- **Task
|
|
18
|
-
- **Phase
|
|
17
|
+
- **Task content**: 5–10 words, what is being done, not how. Used as the task identifier — unique.
|
|
18
|
+
- **Phase name**: short noun phrase (e.g. `Foundation`, `Auth`, `Verification`). Used as the phase identifier — unique. Do not add prefixes like `1.`, `A)`, `Phase 1:`, etc.
|
|
19
19
|
|
|
20
20
|
## Rules
|
|
21
|
-
- Mark tasks done immediately after finishing
|
|
21
|
+
- Mark tasks done immediately after finishing.
|
|
22
22
|
- Complete phases in order.
|
|
23
|
-
- On blockers, `append` a new task to the active phase
|
|
24
|
-
-
|
|
23
|
+
- On blockers, `append` a new task to the active phase to unblock yourself, or `drop`.
|
|
24
|
+
- `task` and `phase` fields reference content/name verbatim; keep them stable once introduced.
|
|
25
25
|
|
|
26
26
|
## When to create a list
|
|
27
27
|
- Task requires 3+ distinct steps
|
|
@@ -31,17 +31,17 @@ The next pending task is auto-promoted to `in_progress` after each completion.
|
|
|
31
31
|
|
|
32
32
|
<examples>
|
|
33
33
|
# Initial setup (multi-phase)
|
|
34
|
-
`{"ops":[{"op":"
|
|
35
|
-
# Initial setup (single phase
|
|
36
|
-
`{"ops":[{"op":"
|
|
34
|
+
`{"ops":[{"op":"init","list":[{"phase":"Foundation","items":["Scaffold crate","Wire workspace"]},{"phase":"Auth","items":["Port credential store","Wire OAuth providers"]},{"phase":"Verification","items":["Run cargo test"]}]}]}`
|
|
35
|
+
# Initial setup (single phase)
|
|
36
|
+
`{"ops":[{"op":"init","list":[{"phase":"Implementation","items":["Apply fix","Run tests"]}]}]}`
|
|
37
37
|
# Complete one task
|
|
38
|
-
`{"ops":[{"op":"done","task":"
|
|
38
|
+
`{"ops":[{"op":"done","task":"Wire workspace"}]}`
|
|
39
39
|
# Complete a whole phase
|
|
40
|
-
`{"ops":[{"op":"done","phase":"
|
|
40
|
+
`{"ops":[{"op":"done","phase":"Auth"}]}`
|
|
41
41
|
# Remove all tasks
|
|
42
42
|
`{"ops":[{"op":"rm"}]}`
|
|
43
43
|
# Drop one task
|
|
44
|
-
`{"ops":[{"op":"drop","task":"
|
|
44
|
+
`{"ops":[{"op":"drop","task":"Run cargo test"}]}`
|
|
45
45
|
# Append tasks to a phase
|
|
46
|
-
`{"ops":[{"op":"append","phase":"
|
|
46
|
+
`{"ops":[{"op":"append","phase":"Auth","items":["Handle retries","Run tests"]}]}`
|
|
47
47
|
</examples>
|