@oh-my-pi/pi-coding-agent 16.2.3 → 16.2.5
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 +31 -0
- package/dist/cli.js +3449 -3438
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/collab/replication-shrink.d.ts +39 -0
- package/dist/types/config/provider-globals.d.ts +7 -0
- package/dist/types/memories/index.d.ts +20 -1
- package/dist/types/modes/components/status-line/component.d.ts +5 -0
- package/dist/types/modes/components/status-line/types.d.ts +10 -0
- package/dist/types/modes/controllers/command-controller.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +7 -0
- package/dist/types/session/session-manager.d.ts +2 -3
- package/dist/types/task/provider-concurrency.d.ts +40 -0
- package/dist/types/utils/git.d.ts +11 -0
- package/package.json +12 -12
- package/src/autolearn/controller.ts +13 -22
- package/src/cli/update-cli.ts +254 -0
- package/src/cli/worktree-cli.ts +8 -5
- package/src/collab/host.ts +13 -8
- package/src/collab/replication-shrink.ts +111 -0
- package/src/config/model-discovery.ts +33 -13
- package/src/config/provider-globals.ts +25 -0
- package/src/config/settings-schema.ts +5 -2
- package/src/eval/__tests__/julia-prelude.test.ts +2 -2
- package/src/memories/index.ts +115 -9
- package/src/memory-backend/local-backend.ts +5 -3
- package/src/modes/components/status-line/component.ts +35 -2
- package/src/modes/components/status-line/segments.ts +22 -8
- package/src/modes/components/status-line/types.ts +7 -0
- package/src/modes/controllers/command-controller.ts +5 -35
- package/src/modes/controllers/event-controller.ts +7 -1
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +9 -20
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +2 -1
- package/src/prompts/goals/goal-todo-context.md +12 -0
- package/src/sdk.ts +13 -5
- package/src/session/agent-session.ts +129 -14
- package/src/session/session-manager.ts +2 -3
- package/src/slash-commands/builtin-registry.ts +7 -21
- package/src/task/executor.ts +4 -62
- package/src/task/provider-concurrency.ts +100 -0
- package/src/task/worktree.ts +13 -4
- package/src/task/yield-assembly.ts +27 -39
- package/src/tools/read.ts +11 -1
- package/src/utils/git.ts +13 -0
- package/src/prompts/system/autolearn-nudge.md +0 -5
package/src/memories/index.ts
CHANGED
|
@@ -147,16 +147,42 @@ export function startMemoryStartupTask(options: {
|
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
interface MemoryInstructionSession {
|
|
151
|
+
sessionManager: Pick<AgentSession["sessionManager"], "getSessionFile">;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface MemoryToolDeveloperInstructionsSnapshot {
|
|
155
|
+
summary: string;
|
|
156
|
+
learned: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface CachedMemoryToolDeveloperInstructions {
|
|
160
|
+
sessionFile: string | undefined;
|
|
161
|
+
snapshot: MemoryToolDeveloperInstructionsSnapshot | undefined;
|
|
162
|
+
value: string | undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const memoryToolDeveloperInstructionsBySession = new WeakMap<
|
|
166
|
+
MemoryInstructionSession,
|
|
167
|
+
CachedMemoryToolDeveloperInstructions
|
|
168
|
+
>();
|
|
169
|
+
const memoryToolDeveloperInstructionsByRoot = new Map<string, MemoryToolDeveloperInstructionsSnapshot | undefined>();
|
|
170
|
+
|
|
171
|
+
function getMemoryInstructionRoot(agentDir: string, settings: Settings): string {
|
|
172
|
+
return getMemoryRoot(agentDir, settings.getCwd());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getMemoryInstructionSessionFile(session: MemoryInstructionSession): string | undefined {
|
|
176
|
+
return session.sessionManager.getSessionFile() ?? undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function readMemoryToolDeveloperInstructionsSnapshot(
|
|
154
180
|
agentDir: string,
|
|
155
181
|
settings: Settings,
|
|
156
|
-
): Promise<
|
|
182
|
+
): Promise<MemoryToolDeveloperInstructionsSnapshot | undefined> {
|
|
157
183
|
const cfg = loadMemoryConfig(settings);
|
|
158
184
|
if (!cfg.enabled) return undefined;
|
|
159
|
-
const memoryRoot =
|
|
185
|
+
const memoryRoot = getMemoryInstructionRoot(agentDir, settings);
|
|
160
186
|
|
|
161
187
|
let summary = "";
|
|
162
188
|
try {
|
|
@@ -166,9 +192,21 @@ export async function buildMemoryToolDeveloperInstructions(
|
|
|
166
192
|
// so any captured lessons still surface on their own.
|
|
167
193
|
}
|
|
168
194
|
const learned = await readLearnedLessons(memoryRoot);
|
|
169
|
-
|
|
195
|
+
return { summary, learned };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function renderMemoryToolDeveloperInstructionsSnapshot(
|
|
199
|
+
snapshot: MemoryToolDeveloperInstructionsSnapshot | undefined,
|
|
200
|
+
settings: Settings,
|
|
201
|
+
): string | undefined {
|
|
202
|
+
if (!snapshot) return undefined;
|
|
203
|
+
const cfg = loadMemoryConfig(settings);
|
|
204
|
+
if (!cfg.enabled) return undefined;
|
|
205
|
+
if (!snapshot.summary && !snapshot.learned) return undefined;
|
|
170
206
|
|
|
171
|
-
const summaryOut = summary
|
|
207
|
+
const summaryOut = snapshot.summary
|
|
208
|
+
? truncateByApproxTokens(snapshot.summary, cfg.summaryInjectionTokenLimit).trim()
|
|
209
|
+
: "";
|
|
172
210
|
// Lessons share ONE injection budget with the summary so the combined block
|
|
173
211
|
// stays within `summaryInjectionTokenLimit` (~4 chars/token, matching
|
|
174
212
|
// truncateByApproxTokens). With no summary, lessons get the whole budget.
|
|
@@ -176,7 +214,8 @@ export async function buildMemoryToolDeveloperInstructions(
|
|
|
176
214
|
// can exceed `limit * 4` chars and drive the remainder negative — when the
|
|
177
215
|
// summary already fills the budget, lessons are simply dropped.
|
|
178
216
|
const learnedBudget = Math.max(0, cfg.summaryInjectionTokenLimit - Math.ceil(summaryOut.length / 4));
|
|
179
|
-
const learnedOut =
|
|
217
|
+
const learnedOut =
|
|
218
|
+
snapshot.learned && learnedBudget > 0 ? truncateByApproxTokens(snapshot.learned, learnedBudget).trim() : "";
|
|
180
219
|
if (!summaryOut && !learnedOut) return undefined;
|
|
181
220
|
|
|
182
221
|
return prompt.render(readPathTemplate, {
|
|
@@ -185,6 +224,72 @@ export async function buildMemoryToolDeveloperInstructions(
|
|
|
185
224
|
});
|
|
186
225
|
}
|
|
187
226
|
|
|
227
|
+
function cacheMemoryToolDeveloperInstructions(
|
|
228
|
+
session: MemoryInstructionSession,
|
|
229
|
+
sessionFile: string | undefined,
|
|
230
|
+
snapshot: MemoryToolDeveloperInstructionsSnapshot | undefined,
|
|
231
|
+
settings: Settings,
|
|
232
|
+
): string | undefined {
|
|
233
|
+
const value = renderMemoryToolDeveloperInstructionsSnapshot(snapshot, settings);
|
|
234
|
+
memoryToolDeveloperInstructionsBySession.set(session, { sessionFile, snapshot, value });
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Drop the per-session memory instruction snapshot after explicit memory state
|
|
240
|
+
* changes that must affect the active conversation immediately, such as
|
|
241
|
+
* `/memory clear`.
|
|
242
|
+
*/
|
|
243
|
+
export function clearMemoryToolDeveloperInstructionsCache(session: MemoryInstructionSession | undefined): void {
|
|
244
|
+
if (session) memoryToolDeveloperInstructionsBySession.delete(session);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Refresh the active session's consolidated-memory snapshot after startup maintenance.
|
|
249
|
+
*
|
|
250
|
+
* Startup may finish after the first prompt build and write `memory_summary.md`;
|
|
251
|
+
* the active session should see that summary. It must not reread `learned.md`,
|
|
252
|
+
* because a `learn` call racing with startup belongs to the next session's
|
|
253
|
+
* memory prompt, not the active prompt-cache prefix.
|
|
254
|
+
*/
|
|
255
|
+
export async function refreshMemoryToolDeveloperInstructionsCacheAfterStartup(
|
|
256
|
+
session: MemoryInstructionSession,
|
|
257
|
+
agentDir: string,
|
|
258
|
+
settings: Settings,
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
const sessionFile = getMemoryInstructionSessionFile(session);
|
|
261
|
+
const cached = memoryToolDeveloperInstructionsBySession.get(session);
|
|
262
|
+
const current = await readMemoryToolDeveloperInstructionsSnapshot(agentDir, settings);
|
|
263
|
+
const root = getMemoryInstructionRoot(agentDir, settings);
|
|
264
|
+
const baseline = memoryToolDeveloperInstructionsByRoot.get(root);
|
|
265
|
+
const cachedLearned = cached && cached.sessionFile === sessionFile ? cached.snapshot?.learned : undefined;
|
|
266
|
+
const learned = cachedLearned ?? baseline?.learned ?? "";
|
|
267
|
+
const snapshot = current ? { summary: current.summary, learned } : undefined;
|
|
268
|
+
cacheMemoryToolDeveloperInstructions(session, sessionFile, snapshot, settings);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Build memory usage instructions for prompt injection.
|
|
273
|
+
*/
|
|
274
|
+
export async function buildMemoryToolDeveloperInstructions(
|
|
275
|
+
agentDir: string,
|
|
276
|
+
settings: Settings,
|
|
277
|
+
session?: MemoryInstructionSession,
|
|
278
|
+
): Promise<string | undefined> {
|
|
279
|
+
if (!session) {
|
|
280
|
+
const snapshot = await readMemoryToolDeveloperInstructionsSnapshot(agentDir, settings);
|
|
281
|
+
memoryToolDeveloperInstructionsByRoot.set(getMemoryInstructionRoot(agentDir, settings), snapshot);
|
|
282
|
+
return renderMemoryToolDeveloperInstructionsSnapshot(snapshot, settings);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const sessionFile = getMemoryInstructionSessionFile(session);
|
|
286
|
+
const cached = memoryToolDeveloperInstructionsBySession.get(session);
|
|
287
|
+
if (cached && cached.sessionFile === sessionFile) return cached.value;
|
|
288
|
+
|
|
289
|
+
const snapshot = await readMemoryToolDeveloperInstructionsSnapshot(agentDir, settings);
|
|
290
|
+
return cacheMemoryToolDeveloperInstructions(session, sessionFile, snapshot, settings);
|
|
291
|
+
}
|
|
292
|
+
|
|
188
293
|
/**
|
|
189
294
|
* Clear all persisted memory state and generated artifacts.
|
|
190
295
|
*/
|
|
@@ -219,6 +324,7 @@ async function runMemoryStartup(options: {
|
|
|
219
324
|
}): Promise<void> {
|
|
220
325
|
await runPhase1(options);
|
|
221
326
|
await runPhase2(options);
|
|
327
|
+
await refreshMemoryToolDeveloperInstructionsCacheAfterStartup(options.session, options.agentDir, options.settings);
|
|
222
328
|
await options.session.refreshBaseSystemPrompt?.();
|
|
223
329
|
}
|
|
224
330
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildMemoryToolDeveloperInstructions,
|
|
3
3
|
clearMemoryData,
|
|
4
|
+
clearMemoryToolDeveloperInstructionsCache,
|
|
4
5
|
enqueueMemoryConsolidation,
|
|
5
6
|
saveLearnedLesson,
|
|
6
7
|
startMemoryStartupTask,
|
|
@@ -20,10 +21,11 @@ export const localBackend: MemoryBackend = {
|
|
|
20
21
|
start(options) {
|
|
21
22
|
startMemoryStartupTask(options);
|
|
22
23
|
},
|
|
23
|
-
async buildDeveloperInstructions(agentDir, settings) {
|
|
24
|
-
return buildMemoryToolDeveloperInstructions(agentDir, settings);
|
|
24
|
+
async buildDeveloperInstructions(agentDir, settings, session) {
|
|
25
|
+
return buildMemoryToolDeveloperInstructions(agentDir, settings, session);
|
|
25
26
|
},
|
|
26
|
-
async clear(agentDir, cwd) {
|
|
27
|
+
async clear(agentDir, cwd, session) {
|
|
28
|
+
clearMemoryToolDeveloperInstructionsCache(session);
|
|
27
29
|
await clearMemoryData(agentDir, cwd);
|
|
28
30
|
},
|
|
29
31
|
async enqueue(agentDir, cwd) {
|
|
@@ -160,6 +160,29 @@ interface ActiveRepoCache {
|
|
|
160
160
|
projectDir: string;
|
|
161
161
|
activeRepo: ActiveRepoContext | null;
|
|
162
162
|
effectiveGitCwd: string;
|
|
163
|
+
/** Project + worktree dir name when `projectDir` is a linked worktree, else null. */
|
|
164
|
+
worktree: WorktreeContext | null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface WorktreeContext {
|
|
168
|
+
/** Primary-checkout (project) name shown by the path segment. */
|
|
169
|
+
projectName: string;
|
|
170
|
+
/** Worktree directory name — suppressed from the path when it equals the branch. */
|
|
171
|
+
worktreeName: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Project + worktree-dir names when `cwd` is a linked git worktree, else null.
|
|
176
|
+
* The project name comes from the shared primary checkout; bare-repo worktrees
|
|
177
|
+
* resolve to the shared `foo.git` dir, so a trailing `.git` is stripped.
|
|
178
|
+
*/
|
|
179
|
+
function resolveWorktreeContext(cwd: string): WorktreeContext | null {
|
|
180
|
+
const worktree = git.repo.linkedWorktreeSync(cwd);
|
|
181
|
+
if (!worktree) return null;
|
|
182
|
+
const base = path.basename(worktree.primaryRoot);
|
|
183
|
+
const projectName = base.endsWith(".git") ? base.slice(0, -4) : base;
|
|
184
|
+
if (!projectName) return null;
|
|
185
|
+
return { projectName, worktreeName: path.basename(worktree.root) };
|
|
163
186
|
}
|
|
164
187
|
|
|
165
188
|
/**
|
|
@@ -312,7 +335,10 @@ export class StatusLineComponent implements Component {
|
|
|
312
335
|
|
|
313
336
|
const activeRepo = resolveActiveRepoContextSync(projectDir);
|
|
314
337
|
const effectiveGitCwd = activeRepo?.repoRoot ?? projectDir;
|
|
315
|
-
|
|
338
|
+
// Only collapse the bare-cwd case: a single-direct-child-repo context
|
|
339
|
+
// (activeRepo set) renders `<parent> ↳ <child>`, which we leave intact.
|
|
340
|
+
const worktree = activeRepo ? null : resolveWorktreeContext(effectiveGitCwd);
|
|
341
|
+
this.#activeRepoCache = { projectDir, activeRepo, effectiveGitCwd, worktree };
|
|
316
342
|
return this.#activeRepoCache;
|
|
317
343
|
}
|
|
318
344
|
|
|
@@ -367,6 +393,12 @@ export class StatusLineComponent implements Component {
|
|
|
367
393
|
this.#subagentCount = count;
|
|
368
394
|
}
|
|
369
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Compatibility shim for callers predating the simplified subagent badge.
|
|
398
|
+
* The status line now intentionally shows only the active count.
|
|
399
|
+
*/
|
|
400
|
+
setSubagentHubHint(_hint: string | undefined): void {}
|
|
401
|
+
|
|
370
402
|
/** Active subagent count as currently displayed (collab state mirroring). */
|
|
371
403
|
get subagentCount(): number {
|
|
372
404
|
return this.#subagentCount;
|
|
@@ -982,7 +1014,7 @@ export class StatusLineComponent implements Component {
|
|
|
982
1014
|
const projectDir = getProjectDir();
|
|
983
1015
|
const activeRepoCache = shouldResolveActiveRepo
|
|
984
1016
|
? this.#resolveActiveRepoCache()
|
|
985
|
-
: { projectDir, activeRepo: null, effectiveGitCwd: projectDir };
|
|
1017
|
+
: { projectDir, activeRepo: null, effectiveGitCwd: projectDir, worktree: null };
|
|
986
1018
|
const gitBranch = includeGit || includePr ? this.#getCurrentBranch(activeRepoCache.effectiveGitCwd) : null;
|
|
987
1019
|
const gitStatus = includeGit ? this.#getGitStatus(activeRepoCache.effectiveGitCwd) : null;
|
|
988
1020
|
const gitPr = includePr ? this.#lookupPr(activeRepoCache.effectiveGitCwd) : null;
|
|
@@ -1009,6 +1041,7 @@ export class StatusLineComponent implements Component {
|
|
|
1009
1041
|
status: gitStatus,
|
|
1010
1042
|
pr: gitPr,
|
|
1011
1043
|
},
|
|
1044
|
+
worktree: activeRepoCache.worktree,
|
|
1012
1045
|
usage: this.#cachedUsage,
|
|
1013
1046
|
};
|
|
1014
1047
|
}
|
|
@@ -20,6 +20,13 @@ function withIcon(icon: string, text: string): string {
|
|
|
20
20
|
return icon ? `${icon} ${text}` : text;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Left-truncate a path/label to `maxLen`, prefixing an ellipsis when clipped. */
|
|
24
|
+
function clampPathLength(pwd: string, maxLen: number): string {
|
|
25
|
+
if (pwd.length <= maxLen) return pwd;
|
|
26
|
+
const ellipsis = "…";
|
|
27
|
+
return `${ellipsis}${pwd.slice(-Math.max(0, maxLen - ellipsis.length))}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
/**
|
|
24
31
|
* Leading glyph of a thinking-level display string (e.g. "◉ xhigh" → "◉").
|
|
25
32
|
* Compact mode promotes this glyph to the model-segment icon so the level
|
|
@@ -220,12 +227,24 @@ const pathSegment: StatusLineSegment = {
|
|
|
220
227
|
id: "path",
|
|
221
228
|
render(ctx) {
|
|
222
229
|
const opts = ctx.options.path ?? {};
|
|
230
|
+
const stripPrefix = opts.stripWorkPrefix !== false;
|
|
231
|
+
|
|
232
|
+
// Linked git worktree: the on-disk path nests the worktree base, the
|
|
233
|
+
// project, and a worktree dir that usually duplicates the branch (already
|
|
234
|
+
// shown by the git segment). Collapse to the project name, appending the
|
|
235
|
+
// worktree dir only when it diverges from the branch.
|
|
236
|
+
if (stripPrefix && ctx.worktree) {
|
|
237
|
+
const { projectName, worktreeName } = ctx.worktree;
|
|
238
|
+
const label = ctx.git.branch === worktreeName ? projectName : `${projectName}/${worktreeName}`;
|
|
239
|
+
const content = withIcon(theme.icon.worktree, clampPathLength(label, opts.maxLength ?? 40));
|
|
240
|
+
return { content: theme.fg("statusLinePath", content), visible: true };
|
|
241
|
+
}
|
|
223
242
|
|
|
224
243
|
const projectDir = ctx.activeRepo?.cwd ?? getProjectDir();
|
|
225
244
|
const { scratch, relative } = classifyProjectDir(projectDir);
|
|
226
245
|
let pwd = projectDir;
|
|
227
246
|
|
|
228
|
-
if (
|
|
247
|
+
if (stripPrefix) {
|
|
229
248
|
if (scratch) {
|
|
230
249
|
if (relative) pwd = relative;
|
|
231
250
|
} else {
|
|
@@ -237,17 +256,12 @@ const pathSegment: StatusLineSegment = {
|
|
|
237
256
|
pwd = shortenPath(pwd);
|
|
238
257
|
}
|
|
239
258
|
|
|
240
|
-
|
|
241
|
-
if (pwd.length > maxLen) {
|
|
242
|
-
const ellipsis = "…";
|
|
243
|
-
const sliceLen = Math.max(0, maxLen - ellipsis.length);
|
|
244
|
-
pwd = `${ellipsis}${pwd.slice(-sliceLen)}`;
|
|
245
|
-
}
|
|
259
|
+
pwd = clampPathLength(pwd, opts.maxLength ?? 40);
|
|
246
260
|
if (repoSuffix) {
|
|
247
261
|
pwd = `${pwd}${repoSuffix}`;
|
|
248
262
|
}
|
|
249
263
|
|
|
250
|
-
const showScratchIcon = scratch &&
|
|
264
|
+
const showScratchIcon = scratch && stripPrefix;
|
|
251
265
|
const icon = showScratchIcon ? theme.icon.scratchFolder : theme.icon.folder;
|
|
252
266
|
const content = withIcon(icon, pwd);
|
|
253
267
|
return { content: theme.fg("statusLinePath", content), visible: true };
|
|
@@ -97,6 +97,13 @@ export interface SegmentContext {
|
|
|
97
97
|
status: { staged: number; unstaged: number; untracked: number } | null;
|
|
98
98
|
pr: { number: number; url: string } | null;
|
|
99
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Set when the path cwd is a *linked* git worktree, naming the shared
|
|
102
|
+
* primary checkout (the project). Lets the path segment collapse the
|
|
103
|
+
* base-prefixed `<base>/<project>/<worktree>` path to the project name —
|
|
104
|
+
* the worktree/branch is already shown by the git segment.
|
|
105
|
+
*/
|
|
106
|
+
worktree: { projectName: string; worktreeName: string } | null;
|
|
100
107
|
usage: {
|
|
101
108
|
tier?: string;
|
|
102
109
|
fiveHour?: { percent: number; resetMinutes?: number };
|
|
@@ -42,7 +42,6 @@ import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
|
|
42
42
|
import type { AuthStorage, OAuthAccountIdentity } from "../../session/auth-storage";
|
|
43
43
|
import type { CompactMode } from "../../session/compact-modes";
|
|
44
44
|
import type { NewSessionOptions } from "../../session/session-entries";
|
|
45
|
-
import { SessionManager } from "../../session/session-manager";
|
|
46
45
|
import { formatShakeSummary, type ShakeMode, type ShakeResult } from "../../session/shake-types";
|
|
47
46
|
import { limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
|
|
48
47
|
import { outputMeta } from "../../tools/output-meta";
|
|
@@ -935,13 +934,13 @@ export class CommandController {
|
|
|
935
934
|
}
|
|
936
935
|
|
|
937
936
|
/**
|
|
938
|
-
* `/move` —
|
|
937
|
+
* `/move` — relocate the current session to a different directory.
|
|
939
938
|
*
|
|
940
939
|
* With no `targetPath` (TUI only), opens an autocomplete overlay so the user
|
|
941
940
|
* can pick or type a directory. With a `targetPath`, resolves it directly.
|
|
942
941
|
* If the target directory does not exist, the user is asked whether to create
|
|
943
|
-
* it.
|
|
944
|
-
*
|
|
942
|
+
* it. The active session file and artifacts are moved into the target
|
|
943
|
+
* directory's session bucket so `/resume` from that directory can find it.
|
|
945
944
|
*/
|
|
946
945
|
async handleMoveCommand(targetPath?: string): Promise<void> {
|
|
947
946
|
if (this.ctx.session.isStreaming) {
|
|
@@ -1003,47 +1002,18 @@ export class CommandController {
|
|
|
1003
1002
|
}
|
|
1004
1003
|
}
|
|
1005
1004
|
|
|
1006
|
-
let newSessionFile: string | undefined;
|
|
1007
1005
|
try {
|
|
1008
|
-
|
|
1009
|
-
// folder, then switch to it. The current session is left behind and
|
|
1010
|
-
// remains resumable via /resume.
|
|
1011
|
-
newSessionFile = SessionManager.createEmptySessionFile(resolvedPath);
|
|
1012
|
-
const switched = await this.ctx.session.switchSession(newSessionFile);
|
|
1013
|
-
if (!switched) {
|
|
1014
|
-
await this.ctx.sessionManager.dropSession(newSessionFile);
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1006
|
+
await this.ctx.sessionManager.moveTo(resolvedPath);
|
|
1017
1007
|
} catch (err) {
|
|
1018
|
-
if (newSessionFile) {
|
|
1019
|
-
try {
|
|
1020
|
-
await this.ctx.sessionManager.dropSession(newSessionFile);
|
|
1021
|
-
} catch (dropErr) {
|
|
1022
|
-
this.ctx.showError(
|
|
1023
|
-
`Move failed: ${err instanceof Error ? err.message : String(err)}; failed to remove empty session: ${dropErr instanceof Error ? dropErr.message : String(dropErr)}`,
|
|
1024
|
-
);
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
1008
|
this.ctx.showError(`Move failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1029
1009
|
return;
|
|
1030
1010
|
}
|
|
1031
1011
|
|
|
1032
|
-
this.ctx.session.markMovedFromEmptySessionFile(newSessionFile!);
|
|
1033
1012
|
await this.ctx.applyCwdChange(resolvedPath);
|
|
1034
1013
|
|
|
1035
|
-
this.ctx.chatContainer.clear();
|
|
1036
|
-
this.ctx.pendingMessagesContainer.clear();
|
|
1037
|
-
this.ctx.compactionQueuedMessages = [];
|
|
1038
|
-
this.ctx.streamingComponent = undefined;
|
|
1039
|
-
this.ctx.streamingMessage = undefined;
|
|
1040
|
-
this.ctx.pendingTools.clear();
|
|
1041
|
-
this.ctx.statusLine.invalidate();
|
|
1042
|
-
this.ctx.statusLine.resetActiveTime();
|
|
1043
|
-
this.ctx.updateEditorTopBorder();
|
|
1044
1014
|
this.ctx.updateEditorBorderColor();
|
|
1045
1015
|
await this.ctx.reloadTodos();
|
|
1046
|
-
this.ctx.ui.requestRender(
|
|
1016
|
+
this.ctx.ui.requestRender();
|
|
1047
1017
|
|
|
1048
1018
|
this.ctx.present([
|
|
1049
1019
|
new Spacer(1),
|
|
@@ -953,9 +953,13 @@ export class EventController {
|
|
|
953
953
|
}
|
|
954
954
|
// Update todo display when todo tool completes
|
|
955
955
|
if (event.toolName === "todo" && !event.isError) {
|
|
956
|
+
const hadTodoReminder = (this.ctx.todoReminderContainer?.children.length ?? 0) > 0;
|
|
957
|
+
this.ctx.todoReminderContainer?.clear();
|
|
956
958
|
const details = event.result.details as { phases?: TodoPhase[] } | undefined;
|
|
957
959
|
if (details?.phases) {
|
|
958
960
|
this.ctx.setTodos(details.phases);
|
|
961
|
+
} else if (hadTodoReminder) {
|
|
962
|
+
this.ctx.ui.requestRender();
|
|
959
963
|
}
|
|
960
964
|
} else if (event.toolName === "todo" && event.isError) {
|
|
961
965
|
const textContent = event.result.content.find(
|
|
@@ -1252,7 +1256,9 @@ export class EventController {
|
|
|
1252
1256
|
|
|
1253
1257
|
async #handleTodoReminder(event: Extract<AgentSessionEvent, { type: "todo_reminder" }>): Promise<void> {
|
|
1254
1258
|
const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
|
|
1255
|
-
this.ctx.
|
|
1259
|
+
this.ctx.todoReminderContainer.clear();
|
|
1260
|
+
this.ctx.todoReminderContainer.addChild(component);
|
|
1261
|
+
this.ctx.ui.requestRender();
|
|
1256
1262
|
}
|
|
1257
1263
|
|
|
1258
1264
|
async #handleTodoAutoClear(_event: Extract<AgentSessionEvent, { type: "todo_auto_clear" }>): Promise<void> {
|
|
@@ -1145,7 +1145,7 @@ export class InputController {
|
|
|
1145
1145
|
|
|
1146
1146
|
/** Send editor text as a follow-up message (queued behind current stream). */
|
|
1147
1147
|
async handleFollowUp(): Promise<void> {
|
|
1148
|
-
let text = this.ctx.editor.
|
|
1148
|
+
let text = this.ctx.editor.getExpandedText().trim();
|
|
1149
1149
|
const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
|
|
1150
1150
|
const imageLinks =
|
|
1151
1151
|
images && this.ctx.editor.pendingImageLinks.length > 0 ? [...this.ctx.editor.pendingImageLinks] : undefined;
|
|
@@ -53,6 +53,7 @@ import { reset as resetCapabilities } from "../capability";
|
|
|
53
53
|
import type { CollabGuestLink } from "../collab/guest";
|
|
54
54
|
import type { CollabHost } from "../collab/host";
|
|
55
55
|
import { KeybindingsManager } from "../config/keybindings";
|
|
56
|
+
import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
|
|
56
57
|
import { isSettingsInitialized, onStatusLineSessionAccentChanged, Settings, settings } from "../config/settings";
|
|
57
58
|
import { clearClaudePluginRootsCache } from "../discovery/helpers";
|
|
58
59
|
import type {
|
|
@@ -100,7 +101,6 @@ import { STTController, type SttState } from "../stt";
|
|
|
100
101
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
|
|
101
102
|
import { formatTaskId } from "../task/render";
|
|
102
103
|
import type { LspStartupServerInfo } from "../tools";
|
|
103
|
-
import { isImageProviderPreference, setPreferredImageProvider } from "../tools/image-gen";
|
|
104
104
|
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
105
105
|
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
|
|
106
106
|
import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
|
|
@@ -114,12 +114,6 @@ import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
|
|
114
114
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
|
|
115
115
|
import { messageHasDisplayableThinking } from "../utils/thinking-display";
|
|
116
116
|
import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
|
|
117
|
-
import {
|
|
118
|
-
isSearchProviderId,
|
|
119
|
-
isSearchProviderPreference,
|
|
120
|
-
setExcludedSearchProviders,
|
|
121
|
-
setPreferredSearchProvider,
|
|
122
|
-
} from "../web/search";
|
|
123
117
|
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
124
118
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
125
119
|
import { ChatBlock, type ChatBlockHost } from "./components/chat-block";
|
|
@@ -384,6 +378,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
384
378
|
chatContainer: TranscriptContainer;
|
|
385
379
|
pendingMessagesContainer: Container;
|
|
386
380
|
statusContainer: Container;
|
|
381
|
+
todoReminderContainer: Container;
|
|
387
382
|
todoContainer: Container;
|
|
388
383
|
subagentContainer: Container;
|
|
389
384
|
btwContainer: Container;
|
|
@@ -552,6 +547,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
552
547
|
this.retryLoader = undefined;
|
|
553
548
|
}
|
|
554
549
|
this.statusContainer.clear();
|
|
550
|
+
this.todoReminderContainer.clear();
|
|
555
551
|
this.pendingMessagesContainer.clear();
|
|
556
552
|
this.#cancelModelCycleClearTimer();
|
|
557
553
|
this.modelCycleContainer.clear();
|
|
@@ -631,6 +627,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
631
627
|
this.chatContainer = new TranscriptContainer();
|
|
632
628
|
this.pendingMessagesContainer = new Container();
|
|
633
629
|
this.statusContainer = new AnchoredLiveContainer();
|
|
630
|
+
this.todoReminderContainer = new AnchoredLiveContainer();
|
|
634
631
|
this.todoContainer = new AnchoredLiveContainer();
|
|
635
632
|
this.subagentContainer = new AnchoredLiveContainer();
|
|
636
633
|
this.btwContainer = new AnchoredLiveContainer();
|
|
@@ -850,6 +847,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
850
847
|
|
|
851
848
|
this.ui.addChild(this.chatContainer);
|
|
852
849
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
850
|
+
this.ui.addChild(this.todoReminderContainer);
|
|
853
851
|
this.ui.addChild(this.todoContainer);
|
|
854
852
|
this.ui.addChild(this.subagentContainer);
|
|
855
853
|
this.ui.addChild(this.btwContainer);
|
|
@@ -1053,18 +1051,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1053
1051
|
// module-level search/image provider state reflects the destination
|
|
1054
1052
|
// project's configuration. Without this, the previous project's
|
|
1055
1053
|
// exclusions leak and newly-excluded providers are still used.
|
|
1056
|
-
|
|
1057
|
-
if (Array.isArray(excludedWebSearchProviders)) {
|
|
1058
|
-
setExcludedSearchProviders(excludedWebSearchProviders.filter(isSearchProviderId));
|
|
1059
|
-
}
|
|
1060
|
-
const webSearchProvider = settings.get("providers.webSearch");
|
|
1061
|
-
if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
|
|
1062
|
-
setPreferredSearchProvider(webSearchProvider);
|
|
1063
|
-
}
|
|
1064
|
-
const imageProvider = settings.get("providers.image");
|
|
1065
|
-
if (isImageProviderPreference(imageProvider)) {
|
|
1066
|
-
setPreferredImageProvider(imageProvider);
|
|
1067
|
-
}
|
|
1054
|
+
applyProviderGlobalsFromSettings(settings);
|
|
1068
1055
|
}
|
|
1069
1056
|
// Re-warm plugin roots, capabilities, slash commands, and the ssh tool so
|
|
1070
1057
|
// the next prompt sees everything scoped to the new project directory.
|
|
@@ -1624,8 +1611,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1624
1611
|
}),
|
|
1625
1612
|
}));
|
|
1626
1613
|
if (!mutated) return;
|
|
1627
|
-
this.todoPhases = next;
|
|
1628
1614
|
this.session.setTodoPhases(next);
|
|
1615
|
+
this.setTodos(next);
|
|
1629
1616
|
}
|
|
1630
1617
|
|
|
1631
1618
|
#cancelTodoAutoClearTimer(): void {
|
|
@@ -4089,12 +4076,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4089
4076
|
},
|
|
4090
4077
|
];
|
|
4091
4078
|
}
|
|
4079
|
+
this.todoReminderContainer.clear();
|
|
4092
4080
|
this.#syncTodoAutoClearTimer();
|
|
4093
4081
|
this.#renderTodoList();
|
|
4094
4082
|
this.ui.requestRender();
|
|
4095
4083
|
}
|
|
4096
4084
|
|
|
4097
4085
|
async reloadTodos(): Promise<void> {
|
|
4086
|
+
this.todoReminderContainer.clear();
|
|
4098
4087
|
await this.#loadTodoList();
|
|
4099
4088
|
this.ui.requestRender();
|
|
4100
4089
|
}
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -96,6 +96,7 @@ export type SymbolKey =
|
|
|
96
96
|
| "icon.pause"
|
|
97
97
|
| "icon.loop"
|
|
98
98
|
| "icon.folder"
|
|
99
|
+
| "icon.worktree"
|
|
99
100
|
| "icon.search"
|
|
100
101
|
| "icon.scratchFolder"
|
|
101
102
|
| "icon.file"
|
|
@@ -299,6 +300,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
299
300
|
"icon.pause": "⏸",
|
|
300
301
|
"icon.loop": "↻",
|
|
301
302
|
"icon.folder": "📁",
|
|
303
|
+
"icon.worktree": "🌳",
|
|
302
304
|
"icon.search": "🔍",
|
|
303
305
|
"icon.scratchFolder": "🗑",
|
|
304
306
|
"icon.file": "📄",
|
|
@@ -561,6 +563,8 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
561
563
|
"icon.search": "\uf002",
|
|
562
564
|
// pick: | alt:
|
|
563
565
|
"icon.scratchFolder": "\uf014",
|
|
566
|
+
// pick: nf-fa-sitemap | alt: nf-cod-list_tree
|
|
567
|
+
"icon.worktree": "\uf0e8",
|
|
564
568
|
// pick: | alt:
|
|
565
569
|
"icon.file": "\uf15b",
|
|
566
570
|
// pick: | alt: ⎇
|
|
@@ -808,6 +812,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
808
812
|
"icon.pause": "||",
|
|
809
813
|
"icon.loop": "loop",
|
|
810
814
|
"icon.folder": "[D]",
|
|
815
|
+
"icon.worktree": "[wt]",
|
|
811
816
|
"icon.search": "[/]",
|
|
812
817
|
"icon.scratchFolder": "[T]",
|
|
813
818
|
"icon.file": "[F]",
|
|
@@ -1797,6 +1802,7 @@ export class Theme {
|
|
|
1797
1802
|
pause: this.#symbols["icon.pause"],
|
|
1798
1803
|
loop: this.#symbols["icon.loop"],
|
|
1799
1804
|
folder: this.#symbols["icon.folder"],
|
|
1805
|
+
worktree: this.#symbols["icon.worktree"],
|
|
1800
1806
|
scratchFolder: this.#symbols["icon.scratchFolder"],
|
|
1801
1807
|
file: this.#symbols["icon.file"],
|
|
1802
1808
|
git: this.#symbols["icon.git"],
|
package/src/modes/types.ts
CHANGED
|
@@ -96,6 +96,7 @@ export interface InteractiveModeContext {
|
|
|
96
96
|
chatContainer: TranscriptContainer;
|
|
97
97
|
pendingMessagesContainer: Container;
|
|
98
98
|
statusContainer: Container;
|
|
99
|
+
todoReminderContainer: Container;
|
|
99
100
|
todoContainer: Container;
|
|
100
101
|
subagentContainer: Container;
|
|
101
102
|
btwContainer: Container;
|
|
@@ -121,7 +122,7 @@ export interface InteractiveModeContext {
|
|
|
121
122
|
focusParentSession(): Promise<void>;
|
|
122
123
|
/** Return the view to the main session (delegates to SessionFocusController.unfocus). */
|
|
123
124
|
unfocusSession(): Promise<void>;
|
|
124
|
-
/** Clear loader,
|
|
125
|
+
/** Clear loader, transient HUD/pending containers, streaming state, and pending tools. */
|
|
125
126
|
clearTransientSessionUi(): void;
|
|
126
127
|
settings: Settings;
|
|
127
128
|
keybindings: KeybindingsManager;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<todo_context>
|
|
2
|
+
Current persisted todo state for this goal follows. Goal continuations do not get a visible user nudge, so treat this as live progress state, not old transcript decoration.
|
|
3
|
+
Before continuing substantial work, compare your next action with these todos. If an item is stale, already finished, or no longer the active pointer, call the `todo` tool first to mark it done or rewrite the list. Do not leave a stale in_progress item while working on later phases.
|
|
4
|
+
|
|
5
|
+
Overall: {{closed}}/{{total}} done, {{open}} open.
|
|
6
|
+
{{#each phases}}
|
|
7
|
+
- {{escapeXml name}}
|
|
8
|
+
{{#each tasks}}
|
|
9
|
+
- [{{status}}] {{escapeXml content}}
|
|
10
|
+
{{/each}}
|
|
11
|
+
{{/each}}
|
|
12
|
+
</todo_context>
|
package/src/sdk.ts
CHANGED
|
@@ -130,6 +130,7 @@ import {
|
|
|
130
130
|
loadProjectContextFiles as loadContextFilesInternal,
|
|
131
131
|
} from "./system-prompt";
|
|
132
132
|
import { AgentOutputManager } from "./task/output-manager";
|
|
133
|
+
import { wrapStreamFnWithProviderConcurrency } from "./task/provider-concurrency";
|
|
133
134
|
import {
|
|
134
135
|
AUTO_THINKING,
|
|
135
136
|
type ConfiguredThinkingLevel,
|
|
@@ -2535,11 +2536,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2535
2536
|
// One-shot launch-latency marker: fired the first time the loop dispatches
|
|
2536
2537
|
// a chat request to the provider transport. See onFirstChatDispatch.
|
|
2537
2538
|
let notifyFirstChatDispatch = options.onFirstChatDispatch;
|
|
2538
|
-
// Shared, settings-aware stream wrapper used by
|
|
2539
|
-
//
|
|
2540
|
-
// sticky-routing variants, antigravity endpoint routing,
|
|
2541
|
-
// and the loop guard consistent across every
|
|
2542
|
-
|
|
2539
|
+
// Shared, settings-aware stream wrapper used by the main agent, advisor,
|
|
2540
|
+
// and side-channel requests (`/btw`, `/omfg`, IRC auto-replies, handoff).
|
|
2541
|
+
// Keeps OpenRouter sticky-routing variants, antigravity endpoint routing,
|
|
2542
|
+
// in-flight caps, and the loop guard consistent across every provider call
|
|
2543
|
+
// the session drives. Wrapped in a per-provider concurrency limiter so
|
|
2544
|
+
// each LLM HTTP request — not the whole subagent lifecycle — holds the
|
|
2545
|
+
// slot, preventing the nested-spawn deadlock from issue #3749.
|
|
2546
|
+
const settingsAwareStreamFn = wrapStreamFnWithProviderConcurrency(
|
|
2547
|
+
settings,
|
|
2548
|
+
createSettingsAwareStreamFn(settings),
|
|
2549
|
+
);
|
|
2543
2550
|
agent = new Agent({
|
|
2544
2551
|
initialState: {
|
|
2545
2552
|
systemPrompt,
|
|
@@ -2709,6 +2716,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2709
2716
|
transformProviderContext,
|
|
2710
2717
|
onPayload,
|
|
2711
2718
|
onResponse,
|
|
2719
|
+
sideStreamFn: settingsAwareStreamFn,
|
|
2712
2720
|
advisorStreamFn: settingsAwareStreamFn,
|
|
2713
2721
|
convertToLlm: convertToLlmFinal,
|
|
2714
2722
|
rebuildSystemPrompt,
|