@oh-my-pi/pi-coding-agent 16.2.4 → 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 +20 -0
- package/dist/cli.js +3367 -3367
- package/dist/types/modes/components/status-line/types.d.ts +10 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +7 -0
- 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/cli/worktree-cli.ts +8 -5
- package/src/config/model-discovery.ts +33 -13
- package/src/config/settings-schema.ts +5 -2
- package/src/modes/components/status-line/component.ts +29 -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/input-controller.ts +1 -1
- package/src/modes/theme/theme.ts +6 -0
- package/src/sdk.ts +13 -5
- package/src/session/agent-session.ts +97 -13
- 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
|
@@ -103,6 +103,16 @@ export interface SegmentContext {
|
|
|
103
103
|
url: string;
|
|
104
104
|
} | null;
|
|
105
105
|
};
|
|
106
|
+
/**
|
|
107
|
+
* Set when the path cwd is a *linked* git worktree, naming the shared
|
|
108
|
+
* primary checkout (the project). Lets the path segment collapse the
|
|
109
|
+
* base-prefixed `<base>/<project>/<worktree>` path to the project name —
|
|
110
|
+
* the worktree/branch is already shown by the git segment.
|
|
111
|
+
*/
|
|
112
|
+
worktree: {
|
|
113
|
+
projectName: string;
|
|
114
|
+
worktreeName: string;
|
|
115
|
+
} | null;
|
|
106
116
|
usage: {
|
|
107
117
|
tier?: string;
|
|
108
118
|
fiveHour?: {
|
|
@@ -6,7 +6,7 @@ export type SymbolPreset = "unicode" | "nerd" | "ascii";
|
|
|
6
6
|
/**
|
|
7
7
|
* All available symbol keys organized by category.
|
|
8
8
|
*/
|
|
9
|
-
export type SymbolKey = "status.success" | "status.error" | "status.warning" | "status.info" | "status.pending" | "status.disabled" | "status.enabled" | "status.running" | "status.shadowed" | "status.aborted" | "status.done" | "nav.cursor" | "nav.selected" | "nav.expand" | "nav.collapse" | "nav.back" | "tree.branch" | "tree.last" | "tree.vertical" | "tree.horizontal" | "tree.hook" | "boxRound.topLeft" | "boxRound.topRight" | "boxRound.bottomLeft" | "boxRound.bottomRight" | "boxRound.horizontal" | "boxRound.vertical" | "boxSharp.topLeft" | "boxSharp.topRight" | "boxSharp.bottomLeft" | "boxSharp.bottomRight" | "boxSharp.horizontal" | "boxSharp.vertical" | "boxSharp.cross" | "boxSharp.teeDown" | "boxSharp.teeUp" | "boxSharp.teeRight" | "boxSharp.teeLeft" | "sep.powerline" | "sep.powerlineThin" | "sep.powerlineLeft" | "sep.powerlineRight" | "sep.powerlineThinLeft" | "sep.powerlineThinRight" | "sep.block" | "sep.space" | "sep.asciiLeft" | "sep.asciiRight" | "sep.dot" | "sep.slash" | "sep.pipe" | "icon.model" | "icon.plan" | "icon.goal" | "icon.pause" | "icon.loop" | "icon.folder" | "icon.search" | "icon.scratchFolder" | "icon.file" | "icon.git" | "icon.branch" | "icon.pr" | "icon.tokens" | "icon.context" | "icon.cost" | "icon.time" | "icon.pi" | "icon.ghost" | "icon.agents" | "icon.job" | "icon.cache" | "icon.cacheMiss" | "icon.input" | "icon.output" | "icon.host" | "icon.session" | "icon.package" | "icon.warning" | "icon.rewind" | "icon.auto" | "icon.fast" | "icon.extensionSkill" | "icon.extensionTool" | "icon.extensionSlashCommand" | "icon.extensionMcp" | "icon.extensionRule" | "icon.extensionHook" | "icon.extensionPrompt" | "icon.extensionContextFile" | "icon.extensionInstruction" | "icon.mic" | "icon.camera" | "thinking.minimal" | "thinking.low" | "thinking.medium" | "thinking.high" | "thinking.xhigh" | "thinking.autoPending" | "checkbox.checked" | "checkbox.unchecked" | "radio.selected" | "radio.unselected" | "format.bullet" | "format.dash" | "format.bracketLeft" | "format.bracketRight" | "md.quoteBorder" | "md.hrChar" | "md.bullet" | "md.colorSwatch" | "lang.default" | "lang.typescript" | "lang.javascript" | "lang.python" | "lang.rust" | "lang.go" | "lang.java" | "lang.c" | "lang.cpp" | "lang.csharp" | "lang.ruby" | "lang.julia" | "lang.php" | "lang.swift" | "lang.kotlin" | "lang.shell" | "lang.html" | "lang.css" | "lang.json" | "lang.yaml" | "lang.markdown" | "lang.sql" | "lang.docker" | "lang.lua" | "lang.text" | "lang.env" | "lang.toml" | "lang.xml" | "lang.ini" | "lang.conf" | "lang.log" | "lang.csv" | "lang.tsv" | "lang.image" | "lang.pdf" | "lang.archive" | "lang.binary" | "tab.appearance" | "tab.model" | "tab.interaction" | "tab.context" | "tab.files" | "tab.shell" | "tab.tools" | "tab.memory" | "tab.tasks" | "tab.providers" | "tool.write" | "tool.edit" | "tool.bash" | "tool.ssh" | "tool.lsp" | "tool.gh" | "tool.webSearch" | "tool.exa" | "tool.browser" | "tool.eval" | "tool.debug" | "tool.mcp" | "tool.job" | "tool.task" | "tool.todo" | "tool.memory" | "tool.ask" | "tool.resolve" | "tool.review" | "tool.inspectImage" | "tool.goal" | "tool.irc" | "tool.delete" | "tool.move";
|
|
9
|
+
export type SymbolKey = "status.success" | "status.error" | "status.warning" | "status.info" | "status.pending" | "status.disabled" | "status.enabled" | "status.running" | "status.shadowed" | "status.aborted" | "status.done" | "nav.cursor" | "nav.selected" | "nav.expand" | "nav.collapse" | "nav.back" | "tree.branch" | "tree.last" | "tree.vertical" | "tree.horizontal" | "tree.hook" | "boxRound.topLeft" | "boxRound.topRight" | "boxRound.bottomLeft" | "boxRound.bottomRight" | "boxRound.horizontal" | "boxRound.vertical" | "boxSharp.topLeft" | "boxSharp.topRight" | "boxSharp.bottomLeft" | "boxSharp.bottomRight" | "boxSharp.horizontal" | "boxSharp.vertical" | "boxSharp.cross" | "boxSharp.teeDown" | "boxSharp.teeUp" | "boxSharp.teeRight" | "boxSharp.teeLeft" | "sep.powerline" | "sep.powerlineThin" | "sep.powerlineLeft" | "sep.powerlineRight" | "sep.powerlineThinLeft" | "sep.powerlineThinRight" | "sep.block" | "sep.space" | "sep.asciiLeft" | "sep.asciiRight" | "sep.dot" | "sep.slash" | "sep.pipe" | "icon.model" | "icon.plan" | "icon.goal" | "icon.pause" | "icon.loop" | "icon.folder" | "icon.worktree" | "icon.search" | "icon.scratchFolder" | "icon.file" | "icon.git" | "icon.branch" | "icon.pr" | "icon.tokens" | "icon.context" | "icon.cost" | "icon.time" | "icon.pi" | "icon.ghost" | "icon.agents" | "icon.job" | "icon.cache" | "icon.cacheMiss" | "icon.input" | "icon.output" | "icon.host" | "icon.session" | "icon.package" | "icon.warning" | "icon.rewind" | "icon.auto" | "icon.fast" | "icon.extensionSkill" | "icon.extensionTool" | "icon.extensionSlashCommand" | "icon.extensionMcp" | "icon.extensionRule" | "icon.extensionHook" | "icon.extensionPrompt" | "icon.extensionContextFile" | "icon.extensionInstruction" | "icon.mic" | "icon.camera" | "thinking.minimal" | "thinking.low" | "thinking.medium" | "thinking.high" | "thinking.xhigh" | "thinking.autoPending" | "checkbox.checked" | "checkbox.unchecked" | "radio.selected" | "radio.unselected" | "format.bullet" | "format.dash" | "format.bracketLeft" | "format.bracketRight" | "md.quoteBorder" | "md.hrChar" | "md.bullet" | "md.colorSwatch" | "lang.default" | "lang.typescript" | "lang.javascript" | "lang.python" | "lang.rust" | "lang.go" | "lang.java" | "lang.c" | "lang.cpp" | "lang.csharp" | "lang.ruby" | "lang.julia" | "lang.php" | "lang.swift" | "lang.kotlin" | "lang.shell" | "lang.html" | "lang.css" | "lang.json" | "lang.yaml" | "lang.markdown" | "lang.sql" | "lang.docker" | "lang.lua" | "lang.text" | "lang.env" | "lang.toml" | "lang.xml" | "lang.ini" | "lang.conf" | "lang.log" | "lang.csv" | "lang.tsv" | "lang.image" | "lang.pdf" | "lang.archive" | "lang.binary" | "tab.appearance" | "tab.model" | "tab.interaction" | "tab.context" | "tab.files" | "tab.shell" | "tab.tools" | "tab.memory" | "tab.tasks" | "tab.providers" | "tool.write" | "tool.edit" | "tool.bash" | "tool.ssh" | "tool.lsp" | "tool.gh" | "tool.webSearch" | "tool.exa" | "tool.browser" | "tool.eval" | "tool.debug" | "tool.mcp" | "tool.job" | "tool.task" | "tool.todo" | "tool.memory" | "tool.ask" | "tool.resolve" | "tool.review" | "tool.inspectImage" | "tool.goal" | "tool.irc" | "tool.delete" | "tool.move";
|
|
10
10
|
export type SpinnerType = "status" | "activity";
|
|
11
11
|
export type ThemeColor = "accent" | "border" | "borderAccent" | "borderMuted" | "success" | "error" | "warning" | "muted" | "dim" | "text" | "thinkingText" | "userMessageText" | "customMessageText" | "customMessageLabel" | "toolTitle" | "toolOutput" | "mdHeading" | "mdLink" | "mdLinkUrl" | "mdCode" | "mdCodeBlock" | "mdCodeBlockBorder" | "mdQuote" | "mdQuoteBorder" | "mdHr" | "mdListBullet" | "toolDiffAdded" | "toolDiffRemoved" | "toolDiffContext" | "syntaxComment" | "syntaxKeyword" | "syntaxFunction" | "syntaxVariable" | "syntaxString" | "syntaxNumber" | "syntaxType" | "syntaxOperator" | "syntaxPunctuation" | "thinkingOff" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh" | "bashMode" | "pythonMode" | "statusLineSep" | "statusLineModel" | "statusLinePath" | "statusLineGitClean" | "statusLineGitDirty" | "statusLineContext" | "statusLineSpend" | "statusLineStaged" | "statusLineDirty" | "statusLineUntracked" | "statusLineOutput" | "statusLineCost" | "statusLineSubagents";
|
|
12
12
|
/** Check if a string is a valid ThemeColor value */
|
|
@@ -164,6 +164,7 @@ export declare class Theme {
|
|
|
164
164
|
pause: string;
|
|
165
165
|
loop: string;
|
|
166
166
|
folder: string;
|
|
167
|
+
worktree: string;
|
|
167
168
|
scratchFolder: string;
|
|
168
169
|
file: string;
|
|
169
170
|
git: string;
|
|
@@ -169,6 +169,13 @@ export interface AgentSessionConfig {
|
|
|
169
169
|
* inherits this so its requests undergo the same shaping as the main turn.
|
|
170
170
|
*/
|
|
171
171
|
transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
172
|
+
/**
|
|
173
|
+
* Stream wrapper passed to side-channel requests (`/btw`, `/omfg`, IRC
|
|
174
|
+
* auto-replies, and handoff generation) so they apply the same provider
|
|
175
|
+
* shaping and host-level request wrappers as normal agent turns. Defaults
|
|
176
|
+
* to plain `streamSimple` when omitted.
|
|
177
|
+
*/
|
|
178
|
+
sideStreamFn?: StreamFn;
|
|
172
179
|
/**
|
|
173
180
|
* Stream wrapper passed to the advisor agent so its requests apply the
|
|
174
181
|
* session's `providers.openrouterVariant`, `providers.antigravityEndpoint`,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-provider LLM concurrency cap, applied around each provider HTTP request.
|
|
3
|
+
*
|
|
4
|
+
* The semaphore brackets only the streaming request itself, not the whole
|
|
5
|
+
* agent lifetime: a parent subagent releases its slot the moment its LLM
|
|
6
|
+
* stream finishes producing, so children spawned during tool execution can
|
|
7
|
+
* acquire slots for their own turns. Holding the slot across the parent's
|
|
8
|
+
* full conversation deadlocks any spawn tree whose width exceeds
|
|
9
|
+
* `maxConcurrency` because the parents wait for children that wait for
|
|
10
|
+
* slots the parents are holding (issue
|
|
11
|
+
* [#3749](https://github.com/can1357/oh-my-pi/issues/3749)).
|
|
12
|
+
*/
|
|
13
|
+
import type { StreamFn } from "@oh-my-pi/pi-agent-core";
|
|
14
|
+
import type { Settings } from "../config/settings";
|
|
15
|
+
import { Semaphore } from "./parallel";
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the configured concurrency ceiling for a provider, or `undefined`
|
|
18
|
+
* when the provider has no cap concept at all. A configured value `<= 0` means
|
|
19
|
+
* "unlimited" and maps to `Infinity` — still a tracked ceiling, so every run
|
|
20
|
+
* holds a slot and a later finite resize counts work started while unlimited.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getProviderConcurrencyLimit(settings: Settings, provider: string): number | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Hand out the single shared limiter for `provider` (creating one lazily) and
|
|
25
|
+
* resize it in place when the configured limit changes. Replacing the
|
|
26
|
+
* semaphore would orphan in-flight slots on the old instance and let a
|
|
27
|
+
* runtime or mixed limit value exceed the cap (issue #3464 review feedback).
|
|
28
|
+
*/
|
|
29
|
+
export declare function getProviderSemaphore(settings: Settings, provider: string): Semaphore | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Wrap a {@link StreamFn} so every LLM HTTP request acquires the provider's
|
|
32
|
+
* concurrency slot before the request goes out and releases it when the
|
|
33
|
+
* stream finishes producing (success, error, or abort). Providers without a
|
|
34
|
+
* configured cap pass straight through.
|
|
35
|
+
*
|
|
36
|
+
* The acquire bracket is intentionally narrow (one slot per LLM call), so
|
|
37
|
+
* spawn trees deeper than `maxConcurrency` no longer deadlock on themselves —
|
|
38
|
+
* see the module-level comment for the failure mode this fixes.
|
|
39
|
+
*/
|
|
40
|
+
export declare function wrapStreamFnWithProviderConcurrency(settings: Settings, base: StreamFn): StreamFn;
|
|
@@ -349,6 +349,17 @@ export declare const repo: {
|
|
|
349
349
|
* the shared common dir (`foo.git`) because they have no primary checkout.
|
|
350
350
|
*/
|
|
351
351
|
primaryRootSync(cwd: string): string | null;
|
|
352
|
+
/**
|
|
353
|
+
* Linked-worktree metadata for `cwd`, or `null` when `cwd` is the primary
|
|
354
|
+
* checkout (or outside a repository). `root` is the worktree's own checkout
|
|
355
|
+
* root; `primaryRoot` is the shared main checkout that names the project.
|
|
356
|
+
* Resolves purely via on-disk `.git`/`commondir` walking — no subprocess —
|
|
357
|
+
* so the status line may call it on every render.
|
|
358
|
+
*/
|
|
359
|
+
linkedWorktreeSync(cwd: string): {
|
|
360
|
+
root: string;
|
|
361
|
+
primaryRoot: string;
|
|
362
|
+
} | null;
|
|
352
363
|
/** Full GitRepository metadata (sync). */
|
|
353
364
|
resolveSync(cwd: string): GitRepository | null;
|
|
354
365
|
/** Full GitRepository metadata. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.2.
|
|
4
|
+
"version": "16.2.5",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -55,17 +55,17 @@
|
|
|
55
55
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
56
56
|
"@babel/parser": "^7.29.7",
|
|
57
57
|
"@mozilla/readability": "^0.6.0",
|
|
58
|
-
"@oh-my-pi/hashline": "16.2.
|
|
59
|
-
"@oh-my-pi/omp-stats": "16.2.
|
|
60
|
-
"@oh-my-pi/pi-agent-core": "16.2.
|
|
61
|
-
"@oh-my-pi/pi-ai": "16.2.
|
|
62
|
-
"@oh-my-pi/pi-catalog": "16.2.
|
|
63
|
-
"@oh-my-pi/pi-mnemopi": "16.2.
|
|
64
|
-
"@oh-my-pi/pi-natives": "16.2.
|
|
65
|
-
"@oh-my-pi/pi-tui": "16.2.
|
|
66
|
-
"@oh-my-pi/pi-utils": "16.2.
|
|
67
|
-
"@oh-my-pi/pi-wire": "16.2.
|
|
68
|
-
"@oh-my-pi/snapcompact": "16.2.
|
|
58
|
+
"@oh-my-pi/hashline": "16.2.5",
|
|
59
|
+
"@oh-my-pi/omp-stats": "16.2.5",
|
|
60
|
+
"@oh-my-pi/pi-agent-core": "16.2.5",
|
|
61
|
+
"@oh-my-pi/pi-ai": "16.2.5",
|
|
62
|
+
"@oh-my-pi/pi-catalog": "16.2.5",
|
|
63
|
+
"@oh-my-pi/pi-mnemopi": "16.2.5",
|
|
64
|
+
"@oh-my-pi/pi-natives": "16.2.5",
|
|
65
|
+
"@oh-my-pi/pi-tui": "16.2.5",
|
|
66
|
+
"@oh-my-pi/pi-utils": "16.2.5",
|
|
67
|
+
"@oh-my-pi/pi-wire": "16.2.5",
|
|
68
|
+
"@oh-my-pi/snapcompact": "16.2.5",
|
|
69
69
|
"@opentelemetry/api": "^1.9.1",
|
|
70
70
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
71
71
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
package/src/cli/worktree-cli.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* containing a `.git` *file* that points back at
|
|
8
8
|
* `<parent-repo>/.git/worktrees/<name>/`.
|
|
9
9
|
* - **Task-isolation dirs** (`task/worktree.ts`): a wrapper dir with a
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* compact `m` subdir mounted/cloned by `natives.isoStart`. Legacy `merged`
|
|
11
|
+
* subdirs are still recognized. These are ephemeral; `ensureIsolation`
|
|
12
|
+
* removes the base before re-creating it, so leftovers are crashed runs.
|
|
13
13
|
*
|
|
14
14
|
* Legacy entries from before the encoding change keep working because git still
|
|
15
15
|
* tracks them by branch name. This command exists to GC them on demand.
|
|
@@ -22,6 +22,8 @@ import * as git from "../utils/git";
|
|
|
22
22
|
|
|
23
23
|
type WorktreeKind = "pr-checkout" | "task-isolation" | "empty" | "stray";
|
|
24
24
|
|
|
25
|
+
const TASK_ISOLATION_MOUNT_DIRS = ["m", "merged"] as const;
|
|
26
|
+
|
|
25
27
|
export interface WorktreeEntry {
|
|
26
28
|
/** Absolute path to the worktree dir (or stray container) under `~/.omp/wt/`. */
|
|
27
29
|
path: string;
|
|
@@ -209,8 +211,9 @@ async function classifyDir(dir: string): Promise<WorktreeEntry | null> {
|
|
|
209
211
|
if (gitStat?.isFile()) {
|
|
210
212
|
return classifyPrCheckout(dir, gitEntry);
|
|
211
213
|
}
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
+
for (const mountDir of TASK_ISOLATION_MOUNT_DIRS) {
|
|
215
|
+
const mountStat = await fs.stat(path.join(dir, mountDir)).catch(() => null);
|
|
216
|
+
if (!mountStat?.isDirectory()) continue;
|
|
214
217
|
return {
|
|
215
218
|
path: dir,
|
|
216
219
|
kind: "task-isolation",
|
|
@@ -126,7 +126,8 @@ type LlamaCppDiscoveredServerMetadata = {
|
|
|
126
126
|
|
|
127
127
|
type LlamaCppModelListEntry = {
|
|
128
128
|
id: string;
|
|
129
|
-
|
|
129
|
+
runtimeContextWindow?: number;
|
|
130
|
+
trainingContextWindow?: number;
|
|
130
131
|
};
|
|
131
132
|
|
|
132
133
|
function toPositiveNumberOrUndefined(value: unknown): number | undefined {
|
|
@@ -142,7 +143,21 @@ function toPositiveNumberOrUndefined(value: unknown): number | undefined {
|
|
|
142
143
|
return undefined;
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
function extractOllamaRuntimeContextWindow(payload: Record<string, unknown>): number | undefined {
|
|
147
|
+
const parameters = payload.parameters;
|
|
148
|
+
if (typeof parameters !== "string") {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const match = parameters.match(/(?:^|\n)\s*num_ctx\s+(\d+)\s*(?:$|\n)/m);
|
|
152
|
+
return match ? toPositiveNumberOrUndefined(match[1]) : undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
145
155
|
function extractOllamaContextWindow(payload: Record<string, unknown>): number | undefined {
|
|
156
|
+
const runtimeContextWindow = extractOllamaRuntimeContextWindow(payload);
|
|
157
|
+
if (runtimeContextWindow !== undefined) {
|
|
158
|
+
return runtimeContextWindow;
|
|
159
|
+
}
|
|
160
|
+
|
|
146
161
|
const modelInfo = payload.model_info;
|
|
147
162
|
if (isRecord(modelInfo)) {
|
|
148
163
|
for (const [key, value] of Object.entries(modelInfo)) {
|
|
@@ -155,12 +170,7 @@ function extractOllamaContextWindow(payload: Record<string, unknown>): number |
|
|
|
155
170
|
}
|
|
156
171
|
}
|
|
157
172
|
|
|
158
|
-
|
|
159
|
-
if (typeof parameters !== "string") {
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
const match = parameters.match(/(?:^|\n)\s*num_ctx\s+(\d+)\s*(?:$|\n)/m);
|
|
163
|
-
return match ? toPositiveNumberOrUndefined(match[1]) : undefined;
|
|
173
|
+
return undefined;
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
function extractLlamaCppContextWindow(payload: Record<string, unknown>): number | undefined {
|
|
@@ -174,12 +184,17 @@ function extractLlamaCppContextWindow(payload: Record<string, unknown>): number
|
|
|
174
184
|
return toPositiveNumberOrUndefined(payload.n_ctx);
|
|
175
185
|
}
|
|
176
186
|
|
|
177
|
-
function
|
|
187
|
+
function extractLlamaCppModelContextWindows(
|
|
188
|
+
item: Record<string, unknown>,
|
|
189
|
+
): Pick<LlamaCppModelListEntry, "runtimeContextWindow" | "trainingContextWindow"> {
|
|
178
190
|
const meta = item.meta;
|
|
179
191
|
if (!isRecord(meta)) {
|
|
180
|
-
return
|
|
192
|
+
return {};
|
|
181
193
|
}
|
|
182
|
-
return
|
|
194
|
+
return {
|
|
195
|
+
runtimeContextWindow: toPositiveNumberOrUndefined(meta.n_ctx),
|
|
196
|
+
trainingContextWindow: toPositiveNumberOrUndefined(meta.n_ctx_train),
|
|
197
|
+
};
|
|
183
198
|
}
|
|
184
199
|
|
|
185
200
|
function parseLlamaCppModelList(payload: unknown): LlamaCppModelListEntry[] {
|
|
@@ -190,7 +205,7 @@ function parseLlamaCppModelList(payload: unknown): LlamaCppModelListEntry[] {
|
|
|
190
205
|
if (!isRecord(item) || typeof item.id !== "string" || !item.id) {
|
|
191
206
|
return [];
|
|
192
207
|
}
|
|
193
|
-
return [{ id: item.id,
|
|
208
|
+
return [{ id: item.id, ...extractLlamaCppModelContextWindows(item) }];
|
|
194
209
|
});
|
|
195
210
|
}
|
|
196
211
|
|
|
@@ -378,7 +393,11 @@ export async function discoverLlamaCppModels(
|
|
|
378
393
|
for (const item of models) {
|
|
379
394
|
const { id } = item;
|
|
380
395
|
if (!id) continue;
|
|
381
|
-
const contextWindow =
|
|
396
|
+
const contextWindow =
|
|
397
|
+
item.runtimeContextWindow ??
|
|
398
|
+
serverMetadata?.contextWindow ??
|
|
399
|
+
item.trainingContextWindow ??
|
|
400
|
+
DISCOVERY_DEFAULT_CONTEXT_WINDOW;
|
|
382
401
|
discovered.push(
|
|
383
402
|
buildModel({
|
|
384
403
|
id,
|
|
@@ -420,7 +439,8 @@ export async function discoverLlamaCppModelContextWindow(
|
|
|
420
439
|
return undefined;
|
|
421
440
|
}
|
|
422
441
|
const entries = parseLlamaCppModelList(await response.json());
|
|
423
|
-
|
|
442
|
+
const entry = entries.find(entry => entry.id === model.id);
|
|
443
|
+
return entry?.runtimeContextWindow ?? entry?.trainingContextWindow;
|
|
424
444
|
};
|
|
425
445
|
try {
|
|
426
446
|
const apiKey = await ctx.getBearerApiKeyResolver(model.provider);
|
|
@@ -317,8 +317,11 @@ export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
|
|
|
317
317
|
// `>` must sit outside quoted regions (so `echo "a -> b"` passes) and be
|
|
318
318
|
// followed by a plausible filename — including `$VAR` targets; `>|`
|
|
319
319
|
// (clobber) counts as a redirect; `>&2`/`2>&1` style fd duplication is
|
|
320
|
-
// not matched.
|
|
321
|
-
|
|
320
|
+
// not matched. Allowed device sinks are consumed while looking for later
|
|
321
|
+
// real file redirects because the write tool cannot replace shell
|
|
322
|
+
// output/discard targets.
|
|
323
|
+
pattern:
|
|
324
|
+
"^\\s*(echo|printf|cat\\s*<<)\\s+(?:(?:[^\"'>]|\"[^\"]*\"|'[^']*')|(?<!\\|)>{1,2}\\|?\\s*(?:\"/dev/(?:null|tty|stdout|stderr)\"|'/dev/(?:null|tty|stdout|stderr)'|/dev/(?:null|tty|stdout|stderr))(?:[\\s;&|]|$))*(?<!\\|)>{1,2}\\|?\\s*(?!(?:\"/dev/(?:null|tty|stdout|stderr)\"|'/dev/(?:null|tty|stdout|stderr)'|/dev/(?:null|tty|stdout|stderr))(?:[\\s;&|]|$))[$\\w./~\"'-]",
|
|
322
325
|
tool: "write",
|
|
323
326
|
message: "Use the `write` tool instead of echo/cat redirection. It handles encoding and provides confirmation.",
|
|
324
327
|
},
|
|
@@ -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
|
|
|
@@ -988,7 +1014,7 @@ export class StatusLineComponent implements Component {
|
|
|
988
1014
|
const projectDir = getProjectDir();
|
|
989
1015
|
const activeRepoCache = shouldResolveActiveRepo
|
|
990
1016
|
? this.#resolveActiveRepoCache()
|
|
991
|
-
: { projectDir, activeRepo: null, effectiveGitCwd: projectDir };
|
|
1017
|
+
: { projectDir, activeRepo: null, effectiveGitCwd: projectDir, worktree: null };
|
|
992
1018
|
const gitBranch = includeGit || includePr ? this.#getCurrentBranch(activeRepoCache.effectiveGitCwd) : null;
|
|
993
1019
|
const gitStatus = includeGit ? this.#getGitStatus(activeRepoCache.effectiveGitCwd) : null;
|
|
994
1020
|
const gitPr = includePr ? this.#lookupPr(activeRepoCache.effectiveGitCwd) : null;
|
|
@@ -1015,6 +1041,7 @@ export class StatusLineComponent implements Component {
|
|
|
1015
1041
|
status: gitStatus,
|
|
1016
1042
|
pr: gitPr,
|
|
1017
1043
|
},
|
|
1044
|
+
worktree: activeRepoCache.worktree,
|
|
1018
1045
|
usage: this.#cachedUsage,
|
|
1019
1046
|
};
|
|
1020
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 };
|
|
@@ -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;
|
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/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,
|