one-code-extension 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -147
- package/docs/guide/README.md +1 -0
- package/docs/guide/configuration.md +2 -2
- package/docs/guide/differences-from-claude-code.md +13 -4
- package/docs/guide/hooks.md +14 -2
- package/docs/guide/installation.md +54 -6
- package/docs/guide/permissions-modes-and-auto-mode.md +23 -7
- package/docs/guide/reference.md +5 -2
- package/docs/guide/sessions-and-context.md +18 -0
- package/docs/guide/terminal-interface.md +1 -1
- package/docs/guide/tools.md +24 -2
- package/docs/guide/troubleshooting.md +14 -4
- package/docs/guide/windows.md +189 -0
- package/extensions/auto-mode/actions.ts +3 -1
- package/extensions/auto-mode/paths.ts +21 -23
- package/extensions/auto-mode/safety-floor.ts +44 -6
- package/extensions/auto-mode/shell-analysis.ts +25 -10
- package/extensions/auto-mode/transcript.ts +15 -5
- package/extensions/background/index.ts +83 -34
- package/extensions/background/monitor-batch.ts +7 -5
- package/extensions/background/wakeup.ts +18 -24
- package/extensions/bash/background.ts +19 -10
- package/extensions/bash/guards.ts +36 -16
- package/extensions/bash/index.ts +36 -166
- package/extensions/branding/index.ts +35 -4
- package/extensions/btw/index.ts +207 -0
- package/extensions/btw/panel.ts +179 -0
- package/extensions/btw/prompt.ts +45 -0
- package/extensions/clear/index.ts +15 -0
- package/extensions/compaction/fit.ts +9 -6
- package/extensions/doctor/build.ts +22 -2
- package/extensions/doctor/dependencies.ts +63 -31
- package/extensions/doctor/fix-prompt.ts +12 -4
- package/extensions/doctor/index.ts +1 -0
- package/extensions/doctor/models.ts +2 -1
- package/extensions/doctor/report.ts +2 -3
- package/extensions/doctor/update-lookup.ts +19 -11
- package/extensions/effort/index.ts +2 -1
- package/extensions/footer/footer-line.ts +19 -0
- package/extensions/footer/index.ts +25 -5
- package/extensions/hooks/executor.ts +84 -18
- package/extensions/hooks/index.ts +1 -0
- package/extensions/hooks/matcher.ts +23 -9
- package/extensions/hooks/settings.ts +15 -0
- package/extensions/lib/branch-restore.ts +12 -0
- package/extensions/lib/claude-context.ts +2 -3
- package/extensions/lib/claude-settings.ts +19 -0
- package/extensions/lib/command-launch.ts +71 -0
- package/extensions/lib/deferred.ts +173 -33
- package/extensions/lib/git.ts +16 -0
- package/extensions/lib/keys.ts +38 -0
- package/extensions/lib/local-command.ts +112 -0
- package/extensions/lib/model-facts.generated.json +263 -68
- package/extensions/lib/model-policy.ts +13 -0
- package/extensions/lib/model-tier.ts +16 -5
- package/extensions/lib/notifications.ts +547 -53
- package/extensions/lib/paths.ts +130 -2
- package/extensions/lib/pi-ai-estimate.ts +57 -26
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/process-tree.ts +131 -5
- package/extensions/lib/reminders.ts +80 -27
- package/extensions/lib/scratchpad.ts +11 -10
- package/extensions/lib/shell-spawn.ts +298 -0
- package/extensions/lib/shell-tool.ts +252 -0
- package/extensions/lib/shell-tools.ts +13 -0
- package/extensions/lib/side-call.ts +76 -0
- package/extensions/lib/tui-render.ts +62 -11
- package/extensions/lib/update-check.mjs +155 -0
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/which.ts +62 -0
- package/extensions/lib/worktree-channel.ts +18 -0
- package/extensions/lib/worktree-isolation.ts +2 -1
- package/extensions/lsp/client.ts +14 -3
- package/extensions/lsp/index.ts +12 -5
- package/extensions/lsp/install-hints.ts +33 -3
- package/extensions/lsp/plugin-servers.ts +2 -2
- package/extensions/lsp/servers.ts +29 -7
- package/extensions/mcp/index.ts +3 -2
- package/extensions/memory/entries.ts +9 -6
- package/extensions/memory/index.ts +3 -2
- package/extensions/memory/open-external.ts +4 -1
- package/extensions/model-default/index.ts +21 -2
- package/extensions/permissions/index.ts +57 -28
- package/extensions/permissions/matcher.ts +120 -33
- package/extensions/permissions/modes.ts +14 -6
- package/extensions/permissions/powershell-rules.ts +549 -0
- package/extensions/permissions/session-grant.ts +9 -9
- package/extensions/plugins/index.ts +3 -2
- package/extensions/plugins/install/install.ts +1 -1
- package/extensions/powershell/description.ts +139 -0
- package/extensions/powershell/guards.ts +91 -0
- package/extensions/powershell/index.ts +89 -0
- package/extensions/powershell/policy.ts +91 -0
- package/extensions/recap/index.ts +10 -24
- package/extensions/recap/prompt.ts +3 -3
- package/extensions/session-title/index.ts +120 -0
- package/extensions/session-title/title.ts +111 -0
- package/extensions/skill/index.ts +40 -18
- package/extensions/skill/invoke.ts +1 -0
- package/extensions/subagents/hand-back-review.ts +10 -12
- package/extensions/subagents/index.ts +221 -78
- package/extensions/subagents/model-select.ts +75 -7
- package/extensions/system-prompt/environment.ts +14 -1
- package/extensions/system-reminder/index.ts +9 -0
- package/extensions/tasks/index.ts +2 -1
- package/extensions/tool-search/index.ts +41 -20
- package/extensions/workflow/agent-session.ts +6 -4
- package/extensions/workflow/index.ts +28 -5
- package/extensions/worktree/guards.ts +5 -4
- package/extensions/worktree/index.ts +15 -2
- package/extensions/worktree/rewrite.ts +17 -0
- package/package.json +9 -5
- package/types/app.d.ts +18 -2
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `powershell` tool's model-facing description: Claude Code's own text
|
|
3
|
+
* (pure).
|
|
4
|
+
*
|
|
5
|
+
* The body is the 2.1.276 capture, byte for byte
|
|
6
|
+
* (`tools/powershell-tool-2.1.276-macos-pwsh7.json`, locked by
|
|
7
|
+
* `test/unit/powershell-description.test.ts`); the edition section is the
|
|
8
|
+
* one slot Claude Code fills per machine — PowerShell 7+ text captured, the
|
|
9
|
+
* Windows PowerShell 5.1 and unknown-edition texts from the binary's JS
|
|
10
|
+
* (identical in the reconstructed `PowerShellTool/prompt.ts`). The
|
|
11
|
+
* "Developer tools verified on this machine's PATH" list Claude Code can add
|
|
12
|
+
* sits behind a remote flag that was off in the capture, so it is not
|
|
13
|
+
* reproduced. One Code appends its own short note about the deferred task
|
|
14
|
+
* tools and the `Start-Sleep` guard, after the captured text, the way the
|
|
15
|
+
* bash tool does.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type PowerShellEditionName = "core" | "desktop" | "unknown";
|
|
19
|
+
|
|
20
|
+
/** Claude Code's edition sections (binary JS, 2.1.276). */
|
|
21
|
+
export const EDITION_SECTIONS: Record<PowerShellEditionName, string> = {
|
|
22
|
+
core: `PowerShell edition: PowerShell 7+ (pwsh)
|
|
23
|
+
- Pipeline chain operators \`&&\` and \`||\` ARE available and work like bash. Prefer \`cmd1 && cmd2\` over \`cmd1; cmd2\` when cmd2 should only run if cmd1 succeeds.
|
|
24
|
+
- Ternary (\`$cond ? $a : $b\`), null-coalescing (\`??\`), and null-conditional (\`?.\`) operators are available.
|
|
25
|
+
- Default file encoding is UTF-8 without BOM.`,
|
|
26
|
+
desktop: `PowerShell edition: Windows PowerShell 5.1 (powershell.exe)
|
|
27
|
+
- Pipeline chain operators \`&&\` and \`||\` are NOT available — they cause a parser error. To run B only if A succeeds: \`A; if ($?) { B }\`. To chain unconditionally: \`A; B\`.
|
|
28
|
+
- Ternary (\`?:\`), null-coalescing (\`??\`), and null-conditional (\`?.\`) operators are NOT available. Use \`if/else\` and explicit \`$null -eq\` checks instead.
|
|
29
|
+
- Avoid \`2>&1\` on native executables. In 5.1, redirecting a native command's stderr inside PowerShell wraps each line in an ErrorRecord (NativeCommandError) and sets \`$?\` to \`$false\` even when the exe returned exit code 0. stderr is already captured for you — don't redirect it.
|
|
30
|
+
- Default file encoding is UTF-16 LE (with BOM). When writing files other tools will read, pass \`-Encoding utf8\` to \`Out-File\`/\`Set-Content\`.
|
|
31
|
+
- \`ConvertFrom-Json\` returns a PSCustomObject, not a hashtable. \`-AsHashtable\` is not available.`,
|
|
32
|
+
unknown: `PowerShell edition: unknown — assume Windows PowerShell 5.1 for compatibility
|
|
33
|
+
- Do NOT use \`&&\`, \`||\`, ternary \`?:\`, null-coalescing \`??\`, or null-conditional \`?.\`. These are PowerShell 7+ only and parser-error on 5.1.
|
|
34
|
+
- To chain commands conditionally: \`A; if ($?) { B }\`. Unconditionally: \`A; B\`.`,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The captured description with the edition section replaced by a slot. */
|
|
38
|
+
const TEMPLATE = `Executes a given PowerShell command with optional timeout. Working directory persists between commands; shell state (variables, functions) does not.
|
|
39
|
+
|
|
40
|
+
IMPORTANT: This tool is for terminal operations via PowerShell: git, npm, docker, and PS cmdlets. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead.
|
|
41
|
+
|
|
42
|
+
{{EDITION}}
|
|
43
|
+
|
|
44
|
+
Before executing the command, please follow these steps:
|
|
45
|
+
|
|
46
|
+
1. Directory Verification:
|
|
47
|
+
- If the command will create new directories or files, first use \`Get-ChildItem\` (or \`ls\`) to verify the parent directory exists and is the correct location
|
|
48
|
+
|
|
49
|
+
2. Command Execution:
|
|
50
|
+
- Always quote file paths that contain spaces with double quotes
|
|
51
|
+
- Capture the output of the command.
|
|
52
|
+
|
|
53
|
+
PowerShell Syntax Notes:
|
|
54
|
+
- Variables use $ prefix: $myVar = "value"
|
|
55
|
+
- Escape character is backtick (\`), not backslash
|
|
56
|
+
- Use Verb-Noun cmdlet naming: Get-ChildItem, Set-Location, New-Item, Remove-Item
|
|
57
|
+
- Common aliases: ls (Get-ChildItem), cd (Set-Location), cat (Get-Content), rm (Remove-Item)
|
|
58
|
+
- Pipe operator | works similarly to bash but passes objects, not text
|
|
59
|
+
- Use Select-Object, Where-Object, ForEach-Object for filtering and transformation
|
|
60
|
+
- String interpolation: "Hello $name" or "Hello $($obj.Property)"
|
|
61
|
+
- Registry access uses PSDrive prefixes: \`HKLM:\\SOFTWARE\\...\`, \`HKCU:\\...\` — NOT raw \`HKEY_LOCAL_MACHINE\\...\`
|
|
62
|
+
- Environment variables: read with \`$env:NAME\`, set with \`$env:NAME = "value"\` (NOT \`Set-Variable\` or bash \`export\`)
|
|
63
|
+
- Call native exe with spaces in path via call operator: \`& "C:\\Program Files\\App\\app.exe" arg1 arg2\`
|
|
64
|
+
|
|
65
|
+
Unix commands that DO NOT exist in PowerShell — use the equivalent instead:
|
|
66
|
+
- head / tail → \`Get-Content file -TotalCount N\` / \`-Tail N\`; piped: \`| Select-Object -First N\` / \`-Last N\`
|
|
67
|
+
- which → \`(Get-Command name).Source\`
|
|
68
|
+
- touch → \`if (-not (Test-Path path)) { New-Item -ItemType File path }\` (NEVER use \`New-Item -Force\` on a file — it truncates existing content)
|
|
69
|
+
- wc -l → \`(Get-Content file | Measure-Object -Line).Lines\`
|
|
70
|
+
- mkdir -p → \`New-Item -ItemType Directory -Force path\` (\`-p\` is not a PowerShell flag)
|
|
71
|
+
- rm -rf → \`Remove-Item -Recurse -Force path\`
|
|
72
|
+
- ln -s → \`New-Item -ItemType SymbolicLink -Path link -Target target\`
|
|
73
|
+
- chmod / chown → not applicable on Windows; use \`icacls\` only if ACL changes are required
|
|
74
|
+
- 2>/dev/null → \`2>$null\` (but stderr is captured for you — usually unnecessary)
|
|
75
|
+
- VAR=x cmd → \`$env:VAR = 'x'; cmd\` (PowerShell has no inline env-var prefix)
|
|
76
|
+
- Bash control flow (\`if [ -f x ]\`, \`for x in *\`, backtick \`\`cmd\`\` substitution) is a parser error — use \`if (Test-Path x)\`, \`foreach ($x in ...)\`, \`$(cmd)\`
|
|
77
|
+
|
|
78
|
+
Exit-code note: \`-ErrorAction SilentlyContinue\` suppresses error OUTPUT but the cmdlet failure still causes this tool to report exit 1. To make a cmdlet failure truly non-fatal, promote it to terminating and swallow it: \`try { Cmdlet ... -ErrorAction Stop } catch {}\` (without \`-ErrorAction Stop\`, non-terminating errors skip the \`catch\` and still exit 1).
|
|
79
|
+
|
|
80
|
+
Interactive and blocking commands (this tool runs with -NonInteractive and stdin attached to the null device — console prompts read EOF or error immediately; GUI prompts can still block until timeout):
|
|
81
|
+
- NEVER use \`Read-Host\`, \`Get-Credential\`, \`Out-GridView\`, \`$Host.UI.PromptForChoice\`, or \`pause\`
|
|
82
|
+
- Destructive cmdlets (\`Remove-Item\`, \`Stop-Process\`, \`Clear-Content\`, etc.) may prompt for confirmation. Add \`-Confirm:$false\` when you intend the action to proceed. Use \`-Force\` for read-only/hidden items.
|
|
83
|
+
- Never use \`git rebase -i\`, \`git add -i\`, or other commands that open an interactive editor
|
|
84
|
+
|
|
85
|
+
Passing multiline strings (commit messages, file content) to native executables:
|
|
86
|
+
- Use a single-quoted here-string so PowerShell does not expand \`$\` or backticks inside. The closing \`'@\` MUST be at column 0 (no leading whitespace) on its own line — indenting it is a parse error:
|
|
87
|
+
<example>
|
|
88
|
+
git commit -m @'
|
|
89
|
+
Commit message here.
|
|
90
|
+
Second line with $literal dollar signs.
|
|
91
|
+
'@
|
|
92
|
+
</example>
|
|
93
|
+
- Use \`@'...'@\` (single-quoted, literal) not \`@"..."@\` (double-quoted, interpolated) unless you need variable expansion
|
|
94
|
+
- For arguments containing \`-\`, \`@\`, or other characters PowerShell parses as operators, use the stop-parsing token: \`git log --% --format=%H\`
|
|
95
|
+
|
|
96
|
+
Usage notes:
|
|
97
|
+
- The command argument is required.
|
|
98
|
+
- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).
|
|
99
|
+
- It is very helpful if you write a clear, concise description of what this command does.
|
|
100
|
+
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
|
|
101
|
+
- You can use the \`run_in_background\` parameter to run the command in the background. Only use this if you don't need the result immediately and are OK being notified when the command completes later. You do not need to check the output right away - you'll be notified when it finishes.
|
|
102
|
+
- Avoid using PowerShell to run commands that have dedicated tools, unless explicitly instructed:
|
|
103
|
+
- File search: Use Glob (NOT Get-ChildItem -Recurse)
|
|
104
|
+
- Content search: Use Grep (NOT Select-String)
|
|
105
|
+
- Read files: Use Read (NOT Get-Content)
|
|
106
|
+
- Edit files: Use Edit
|
|
107
|
+
- Write files: Use Write (NOT Set-Content/Out-File)
|
|
108
|
+
- Communication: Output text directly (NOT Write-Output/Write-Host)
|
|
109
|
+
- When issuing multiple commands:
|
|
110
|
+
- If the commands are independent and can run in parallel, make multiple PowerShell tool calls in a single message.
|
|
111
|
+
- If the commands depend on each other and must run sequentially, chain them in a single PowerShell call (see edition-specific chaining syntax above).
|
|
112
|
+
- Use \`;\` only when you need to run commands sequentially but don't care if earlier commands fail.
|
|
113
|
+
- DO NOT use newlines to separate commands (newlines are ok in quoted strings and here-strings)
|
|
114
|
+
- Do NOT prefix commands with \`cd\` or \`Set-Location\` -- the working directory is already set to the correct project directory automatically.
|
|
115
|
+
- Avoid unnecessary \`Start-Sleep\` commands:
|
|
116
|
+
- Do not sleep between commands that can run immediately — just run them.
|
|
117
|
+
- If your command is long running and you would like to be notified when it finishes — simply run your command using \`run_in_background\`. There is no need to sleep in this case.
|
|
118
|
+
- Do not retry failing commands in a sleep loop — diagnose the root cause or consider an alternative approach.
|
|
119
|
+
- If waiting for a background task you started with \`run_in_background\`, you will be notified when it completes — do not poll.
|
|
120
|
+
- If you must poll an external process, use a check command rather than sleeping first.
|
|
121
|
+
- If you must sleep, keep the duration short to avoid blocking the user.
|
|
122
|
+
- For git commands:
|
|
123
|
+
- Prefer to create a new commit rather than amending an existing commit.
|
|
124
|
+
- Before running destructive operations (e.g., git reset --hard, git push --force, git checkout --), consider whether there is a safer alternative that achieves the same goal. Only use destructive operations when they are truly the best approach.
|
|
125
|
+
- Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign, -c commit.gpgsign=false) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.`;
|
|
126
|
+
|
|
127
|
+
/** One Code's addendum: where the harness differs from Claude Code's runtime (task tools are deferred, the sleep guard). */
|
|
128
|
+
export const ONE_CODE_ADDENDUM =
|
|
129
|
+
" A background run returns a task id immediately; completion arrives as a task notification, and the output is retrievable with task_output / stoppable with task_stop (both deferred — load them with tool_search; in a one-shot print/json session the call runs to completion and returns the output directly). A foreground command that LEADS with `Start-Sleep` is blocked; to wait on a condition use the monitor tool (deferred — load it with tool_search select:monitor).";
|
|
130
|
+
|
|
131
|
+
/** Claude Code's description for the detected edition, verbatim, without One Code's addendum. */
|
|
132
|
+
export function claudeCodePowerShellDescription(edition: PowerShellEditionName): string {
|
|
133
|
+
return TEMPLATE.replace("{{EDITION}}", EDITION_SECTIONS[edition]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The description the `powershell` tool ships: Claude Code's text plus One Code's addendum. */
|
|
137
|
+
export function powerShellToolDescription(edition: PowerShellEditionName): string {
|
|
138
|
+
return `${claudeCodePowerShellDescription(edition)}${ONE_CODE_ADDENDUM}`;
|
|
139
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic pre-execution guards for the powershell tool (pure) — the
|
|
3
|
+
* PowerShell spelling of `bash/guards.ts`'s wait and interactive guards, on
|
|
4
|
+
* the same shared `leadingSleepReason` core. Steering, not security: the
|
|
5
|
+
* permission gate and the auto-mode classifier still judge everything that
|
|
6
|
+
* passes. A guard concludes only on a positive parse, so an unparseable line
|
|
7
|
+
* always passes through.
|
|
8
|
+
*
|
|
9
|
+
* - wait guard (foreground): a line that LEADS with `Start-Sleep` (or its
|
|
10
|
+
* `sleep` alias) exists only to wait and stalls the session; a provably
|
|
11
|
+
* short sleep (< 2 s) is pacing and passes.
|
|
12
|
+
* - interactive guard (foreground + background): `Read-Host`, `Get-Credential`,
|
|
13
|
+
* `Out-GridView`, `pause` and interactive git open a prompt this
|
|
14
|
+
* -NonInteractive shell answers with EOF or an error — the tool description
|
|
15
|
+
* already forbids them; the guard makes the refusal instructive.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { leadingSleepReason } from "../bash/guards.ts";
|
|
19
|
+
import { gitSubcommand } from "../auto-mode/shell-analysis.ts";
|
|
20
|
+
import { powershellStatements, statementCommand } from "../permissions/powershell-rules.ts";
|
|
21
|
+
|
|
22
|
+
/** Seconds a `Start-Sleep` statement provably lasts, or undefined when its argument is not a literal. */
|
|
23
|
+
export function startSleepSeconds(statement: string): number | undefined {
|
|
24
|
+
const tokens = statement.trim().split(/\s+/).slice(1);
|
|
25
|
+
if (tokens.length === 0) return undefined;
|
|
26
|
+
let unit: "s" | "ms" = "s";
|
|
27
|
+
let value: number | undefined;
|
|
28
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
29
|
+
const token = tokens[i];
|
|
30
|
+
const named = /^-(s|sec|seconds|ms|milliseconds)(?::(.+))?$/i.exec(token);
|
|
31
|
+
if (named) {
|
|
32
|
+
unit = named[1].toLowerCase().startsWith("m") ? "ms" : "s";
|
|
33
|
+
const inline = named[2] ?? tokens[++i];
|
|
34
|
+
if (inline === undefined) return undefined;
|
|
35
|
+
if (!/^\d+(\.\d+)?$/.test(inline)) return undefined;
|
|
36
|
+
value = Number(inline);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (/^-d(uration)?$/i.test(token)) return undefined; // a TimeSpan literal — not modelled
|
|
40
|
+
if (!/^\d+(\.\d+)?$/.test(token)) return undefined;
|
|
41
|
+
value = Number(token);
|
|
42
|
+
}
|
|
43
|
+
if (value === undefined) return undefined;
|
|
44
|
+
return unit === "ms" ? value / 1000 : value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const INTERACTIVE = new Set(["read-host", "get-credential", "out-gridview", "pause"]);
|
|
48
|
+
|
|
49
|
+
export function powershellGuardReason(command: string, opts: { background: boolean }): string | undefined {
|
|
50
|
+
const statements = powershellStatements(command);
|
|
51
|
+
if (!statements || statements.length === 0) return undefined;
|
|
52
|
+
|
|
53
|
+
for (const statement of statements) {
|
|
54
|
+
const cmd = statementCommand(statement);
|
|
55
|
+
if (INTERACTIVE.has(cmd)) {
|
|
56
|
+
return (
|
|
57
|
+
`Blocked: \`${cmd}\` waits on a console or GUI prompt, and this shell runs -NonInteractive with stdin on the null device — it would error or hang until the timeout. ` +
|
|
58
|
+
"Ask the user with ask_user_question instead, or pass the value on the command line."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (cmd === "git") {
|
|
62
|
+
// `gitSubcommand` skips git's value-taking global flags (`-C <dir>`,
|
|
63
|
+
// `--git-dir <dir>`, `-c <key=val>`, …) so `git -C X rebase -i` still
|
|
64
|
+
// finds "rebase" instead of misreading the flag's value as the
|
|
65
|
+
// subcommand and missing the interactive-editor guard entirely.
|
|
66
|
+
const words = statement.trim().split(/\s+/).slice(1);
|
|
67
|
+
const { sub, rest } = gitSubcommand(words.map((value) => ({ value, hadExpansion: false })));
|
|
68
|
+
const flags = new Set(rest.map((t) => t.value));
|
|
69
|
+
if (sub === "rebase" && (flags.has("-i") || flags.has("--interactive"))) {
|
|
70
|
+
return (
|
|
71
|
+
"Blocked: `git rebase -i` opens an interactive editor, which this shell cannot provide — it would hang until the timeout. " +
|
|
72
|
+
"Drive it non-interactively instead: `$env:GIT_SEQUENCE_EDITOR = ':'; git rebase …` accepts the todo list as-is, or restructure history with `git rebase --onto`, `git commit --amend`, or `git cherry-pick`."
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (sub === "add" && ["-i", "--interactive", "-p", "--patch"].some((f) => flags.has(f))) {
|
|
76
|
+
return (
|
|
77
|
+
"Blocked: interactive `git add` prompts on a TTY this shell does not have — it would read EOF and stage nothing while appearing to succeed. " +
|
|
78
|
+
"Stage whole files with `git add <paths>`, or stage a partial change non-interactively with `git apply --cached` and a crafted diff."
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (opts.background) return undefined;
|
|
84
|
+
|
|
85
|
+
return leadingSleepReason(
|
|
86
|
+
statements,
|
|
87
|
+
(statement) => (statementCommand(statement) === "start-sleep" ? startSleepSeconds(statement) : null),
|
|
88
|
+
(statement) => statement,
|
|
89
|
+
{ sleepName: "Start-Sleep", joiner: "; ", untilExample: "while (-not (<check>)) { Start-Sleep 2 }" },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* powershell extension — Claude Code's PowerShell tool on top of pi's own
|
|
3
|
+
* `powershell` built-in (docs/decisions/windows.md, findings §22).
|
|
4
|
+
*
|
|
5
|
+
* Registering a tool named `powershell` overrides pi's built-in (findings §2),
|
|
6
|
+
* exactly as `extensions/bash` overrides `bash`, and the shared body in
|
|
7
|
+
* lib/shell-tool.ts supplies the `run_in_background` orchestration for both.
|
|
8
|
+
* The foreground path delegates to pi's real definition
|
|
9
|
+
* (`createPowerShellToolDefinition`) so schema, cwd handling, truncation and
|
|
10
|
+
* streaming stay upstream's — with One Code's own `operations`
|
|
11
|
+
* (lib/shell-spawn.ts), because pi's local operations throw off Windows and
|
|
12
|
+
* this tool is developed against a `pwsh` on this Mac. The description is
|
|
13
|
+
* Claude Code's captured text (description.ts); the guards are the PowerShell
|
|
14
|
+
* spelling of bash's.
|
|
15
|
+
*
|
|
16
|
+
* Activation follows Claude Code's gate (policy.ts): pi never activates a
|
|
17
|
+
* registered override of a built-in name on its own, so `session_start` and
|
|
18
|
+
* `model_select` reconcile the active list — `powershell` on when the policy
|
|
19
|
+
* says so, `bash` off when no bash exists (Windows without Git for Windows).
|
|
20
|
+
* Children get the same reconcile: `openChildSession` binds extensions, which
|
|
21
|
+
* emits `session_start`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import { createPowerShellToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { Type } from "typebox";
|
|
27
|
+
import { perCwd } from "../lib/per-cwd.ts";
|
|
28
|
+
import { bashSpawn, createPowerShellOperations, POWERSHELL_UTF8_PREFIX, powerShellEdition, powerShellSpawn } from "../lib/shell-spawn.ts";
|
|
29
|
+
import { registerShellTool } from "../lib/shell-tool.ts";
|
|
30
|
+
import { powerShellToolDescription } from "./description.ts";
|
|
31
|
+
import { powershellGuardReason } from "./guards.ts";
|
|
32
|
+
import { shellToolPolicy, withShellTools } from "./policy.ts";
|
|
33
|
+
|
|
34
|
+
/** Claude Code's PowerShell schema (captured), minus `dangerouslyDisableSandbox` — One Code has no sandbox to disable. */
|
|
35
|
+
const PowerShellParams = Type.Object({
|
|
36
|
+
command: Type.String({ description: "The PowerShell command to execute" }),
|
|
37
|
+
timeout: Type.Optional(Type.Number({ description: "Optional timeout in milliseconds (max 600000)" })),
|
|
38
|
+
description: Type.Optional(Type.String({ description: "Clear, concise description of what this command does in active voice." })),
|
|
39
|
+
run_in_background: Type.Optional(Type.Boolean({ description: "Set to true to run this command in the background." })),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export default function powershellExtension(pi: ExtensionAPI) {
|
|
43
|
+
const spec = powerShellSpawn();
|
|
44
|
+
const operations = createPowerShellOperations(() => spec);
|
|
45
|
+
const base = createPowerShellToolDefinition(process.cwd(), { operations });
|
|
46
|
+
const policy = () =>
|
|
47
|
+
shellToolPolicy({ platform: process.platform, env: process.env, bash: !!bashSpawn().spawn, powershell: spec !== undefined });
|
|
48
|
+
const startupPolicy = policy();
|
|
49
|
+
|
|
50
|
+
registerShellTool(pi, {
|
|
51
|
+
name: "powershell",
|
|
52
|
+
ccLabel: "PowerShell",
|
|
53
|
+
description: powerShellToolDescription(powerShellEdition(spec)),
|
|
54
|
+
parameters: PowerShellParams,
|
|
55
|
+
base,
|
|
56
|
+
// The guideline joins the system prompt's tool guidelines at registration
|
|
57
|
+
// (before the first request), so it is part of the cached prefix, never a
|
|
58
|
+
// mid-session rebuild. Claude Code names PowerShell the primary shell when
|
|
59
|
+
// the tool is on (2.1.126).
|
|
60
|
+
promptGuidelines: [
|
|
61
|
+
...(base.promptGuidelines ?? []),
|
|
62
|
+
...(startupPolicy.primary === "powershell"
|
|
63
|
+
? ["PowerShell is the primary shell on this machine: use the powershell tool for terminal operations (git, npm, build tools)"]
|
|
64
|
+
: []),
|
|
65
|
+
],
|
|
66
|
+
foreground: perCwd((cwd: string) => createPowerShellToolDefinition(cwd, { operations })),
|
|
67
|
+
guard: powershellGuardReason,
|
|
68
|
+
backgroundShell: () => spec,
|
|
69
|
+
// The foreground path prepends pi's UTF-8 prefix inside the operations; the background spawn needs it too.
|
|
70
|
+
wrapBackgroundCommand: (command) => `${POWERSHELL_UTF8_PREFIX}${command}`,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let noticesShown = false;
|
|
74
|
+
const reconcile = (ctx?: Pick<ExtensionContext, "hasUI" | "ui">) => {
|
|
75
|
+
const current = policy();
|
|
76
|
+
const active = pi.getActiveTools();
|
|
77
|
+
const next = withShellTools(active, current);
|
|
78
|
+
if (next.join(" ") !== active.join(" ")) pi.setActiveTools(next);
|
|
79
|
+
if (noticesShown || current.notices.length === 0) return;
|
|
80
|
+
noticesShown = true;
|
|
81
|
+
for (const notice of current.notices) {
|
|
82
|
+
if (ctx?.hasUI) ctx.ui.notify(notice, "warning");
|
|
83
|
+
else process.stderr.write(`${notice}\n`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
pi.on("session_start", (_event, ctx) => reconcile(ctx));
|
|
88
|
+
pi.on("model_select", () => reconcile());
|
|
89
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When the `powershell` tool is on, and which shell tool is primary (pure).
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's gate (2.1.276 binary function `f0`, findings §22):
|
|
5
|
+
* `CLAUDE_CODE_USE_POWERSHELL_TOOL` wins wherever it is set — truthy turns the
|
|
6
|
+
* tool on, `0`/`false` off. Unset: on Windows the tool is on (Git Bash absent
|
|
7
|
+
* → on by necessity; Git Bash present → on, the claude.ai/Console default the
|
|
8
|
+
* docs describe — plan §Open questions 1, resolved "on"); off Windows it is
|
|
9
|
+
* off, so a Mac or Linux user opts in with the same variable Claude Code
|
|
10
|
+
* uses. When it is on, PowerShell is the primary shell (CC 2.1.126) and bash
|
|
11
|
+
* stays alongside when a bash exists. A Windows box with neither shell fails
|
|
12
|
+
* loud at startup in Claude Code's words.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type PowerShellSwitch = "on" | "off" | "unset";
|
|
16
|
+
|
|
17
|
+
/** `CLAUDE_CODE_USE_POWERSHELL_TOOL` as Claude Code reads it: unset/empty → unset, `0`/`false`/`no`/`off` → off, anything else → on. */
|
|
18
|
+
export function powerShellSwitch(env: NodeJS.ProcessEnv): PowerShellSwitch {
|
|
19
|
+
const raw = env.CLAUDE_CODE_USE_POWERSHELL_TOOL?.trim().toLowerCase();
|
|
20
|
+
if (raw === undefined || raw === "") return "unset";
|
|
21
|
+
return ["0", "false", "no", "off"].includes(raw) ? "off" : "on";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ShellAvailability {
|
|
25
|
+
platform: NodeJS.Platform;
|
|
26
|
+
env: NodeJS.ProcessEnv;
|
|
27
|
+
/** A bash resolved (Git Bash on Windows, /bin/bash elsewhere). */
|
|
28
|
+
bash: boolean;
|
|
29
|
+
/** A PowerShell resolved (pwsh/powershell.exe on Windows, pwsh on PATH elsewhere). */
|
|
30
|
+
powershell: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type PrimaryShell = "powershell" | "bash" | "none";
|
|
34
|
+
|
|
35
|
+
export interface ShellToolPolicy {
|
|
36
|
+
/** The `powershell` tool is active. */
|
|
37
|
+
powershell: boolean;
|
|
38
|
+
/** The `bash` tool is active. */
|
|
39
|
+
bash: boolean;
|
|
40
|
+
primary: PrimaryShell;
|
|
41
|
+
/** Startup notices for the user (a set switch with no PowerShell; no shell at all). */
|
|
42
|
+
notices: string[];
|
|
43
|
+
/**
|
|
44
|
+
* The powershell tool is meant to be on here (the env switch, else Windows),
|
|
45
|
+
* whether or not an executable was found. The doctor reports a missing
|
|
46
|
+
* PowerShell as needed only when this is true.
|
|
47
|
+
*/
|
|
48
|
+
powershellWanted: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* A bash is part of this platform's expected shape: everywhere but Windows,
|
|
51
|
+
* where Git for Windows is optional. The doctor's "needed" versus "optional"
|
|
52
|
+
* for bash reads this rather than re-deriving the platform rule.
|
|
53
|
+
*/
|
|
54
|
+
bashExpected: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Claude Code's own wording for a Windows machine with no shell tool. */
|
|
58
|
+
export const NO_SHELL_NOTICE =
|
|
59
|
+
"No shell available: install Git for Windows (https://git-scm.com/download/win) so the bash tool can run, " +
|
|
60
|
+
"or install PowerShell and set CLAUDE_CODE_USE_POWERSHELL_TOOL=1.";
|
|
61
|
+
|
|
62
|
+
export function shellToolPolicy(a: ShellAvailability): ShellToolPolicy {
|
|
63
|
+
const sw = powerShellSwitch(a.env);
|
|
64
|
+
const wanted = sw === "on" ? true : sw === "off" ? false : a.platform === "win32";
|
|
65
|
+
const powershell = wanted && a.powershell;
|
|
66
|
+
const notices: string[] = [];
|
|
67
|
+
if (sw === "on" && !a.powershell) {
|
|
68
|
+
notices.push(
|
|
69
|
+
a.platform === "win32"
|
|
70
|
+
? "CLAUDE_CODE_USE_POWERSHELL_TOOL is set but no pwsh.exe or powershell.exe was found on PATH; the powershell tool stays off."
|
|
71
|
+
: "CLAUDE_CODE_USE_POWERSHELL_TOOL is set but no `pwsh` was found on PATH; install PowerShell 7 to use the powershell tool.",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const bash = a.bash;
|
|
75
|
+
const primary: PrimaryShell = powershell ? "powershell" : bash ? "bash" : "none";
|
|
76
|
+
if (primary === "none" && a.platform === "win32") notices.push(NO_SHELL_NOTICE);
|
|
77
|
+
return { powershell, bash, primary, notices, powershellWanted: wanted, bashExpected: a.platform !== "win32" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The active-tool list adjusted for the policy: `powershell` appended when
|
|
82
|
+
* on (pi never activates it on its own — a registered override of a built-in
|
|
83
|
+
* name does not join the active set), removed when off; `bash` removed when
|
|
84
|
+
* no bash exists (its executor would throw on every call). Existing order is
|
|
85
|
+
* kept so the rebuilt system prompt stays byte-stable.
|
|
86
|
+
*/
|
|
87
|
+
export function withShellTools(active: string[], policy: Pick<ShellToolPolicy, "powershell" | "bash">): string[] {
|
|
88
|
+
const next = active.filter((name) => (name === "powershell" ? policy.powershell : name === "bash" ? policy.bash : true));
|
|
89
|
+
if (policy.powershell && !next.includes("powershell")) next.push("powershell");
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
28
|
-
import type { Api, Model
|
|
28
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
29
29
|
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
30
30
|
import { convertToLlm, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
31
31
|
import { withReasoningFallback } from "../lib/model-policy.ts";
|
|
32
32
|
import { recordUsage } from "../lib/usage-bus.ts";
|
|
33
33
|
import { pickEconomicalContainedModel } from "../lib/model-tier.ts";
|
|
34
|
+
import { answerText, stripImageBlocks, toolStubs } from "../lib/side-call.ts";
|
|
34
35
|
import { dimMarkedLine } from "../lib/tui-render.ts";
|
|
35
36
|
import { RECAP_PROMPT, recapLine, recentForRecap, REFERENCE_MARK } from "./prompt.ts";
|
|
36
37
|
import { RecapScheduler } from "./scheduler.ts";
|
|
@@ -38,6 +39,8 @@ import { RecapScheduler } from "./scheduler.ts";
|
|
|
38
39
|
const ENTRY_TYPE = "one-code:recap";
|
|
39
40
|
const DEFAULT_IDLE_MS = 5 * 60_000; // CC's BLUR_DELAY_MS
|
|
40
41
|
const RECAP_MAX_TOKENS = 256; // 1-3 short sentences
|
|
42
|
+
/** Description on the name-only tool stubs sent with the recap. */
|
|
43
|
+
const STUB_REASON = "Unavailable during this summary; answer in text.";
|
|
41
44
|
const RECAP_TIMEOUT_MS = 30_000;
|
|
42
45
|
|
|
43
46
|
/** The idle delay before a recap, overridable via CC_RECAP_IDLE_MS (floor 1s). */
|
|
@@ -50,22 +53,6 @@ interface RecapData {
|
|
|
50
53
|
content: string;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
/**
|
|
54
|
-
* Name-only tool declarations: enough for a provider to accept the tool_use
|
|
55
|
-
* blocks already in the history, without the descriptions and schemas (the
|
|
56
|
-
* recap never calls a tool, and its prompt asks for text).
|
|
57
|
-
*/
|
|
58
|
-
export function toolStubs(names: readonly string[]): Tool[] {
|
|
59
|
-
return names.map(
|
|
60
|
-
(name) =>
|
|
61
|
-
({
|
|
62
|
-
name,
|
|
63
|
-
description: "Unavailable during this summary; answer in text.",
|
|
64
|
-
parameters: { type: "object", properties: {} },
|
|
65
|
-
}) as unknown as Tool,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
56
|
export default function recapExtension(pi: ExtensionAPI) {
|
|
70
57
|
pi.registerEntryRenderer<RecapData>(ENTRY_TYPE, (entry, _options, theme) => {
|
|
71
58
|
const content = entry.data?.content;
|
|
@@ -123,10 +110,13 @@ export default function recapExtension(pi: ExtensionAPI) {
|
|
|
123
110
|
// blocks stays valid on strict providers (see the header note) without
|
|
124
111
|
// shipping every full schema (~6k tokens) to a call that must answer
|
|
125
112
|
// in text.
|
|
126
|
-
const tools = toolStubs(pi.getActiveTools());
|
|
113
|
+
const tools = toolStubs(pi.getActiveTools(), STUB_REASON);
|
|
127
114
|
|
|
128
115
|
const recent = recentForRecap(messages);
|
|
129
|
-
|
|
116
|
+
// The cheap reader may be a text-only model, and the recap does not need
|
|
117
|
+
// images — strip them so a pasted image or one a Read returned cannot break
|
|
118
|
+
// the call (docs/decisions/model-policy.md).
|
|
119
|
+
const recapMessages = [...stripImageBlocks(convertToLlm(recent)), { role: "user" as const, content: RECAP_PROMPT, timestamp: Date.now() }];
|
|
130
120
|
// Thinking off unless the model cannot disable it; withReasoningFallback
|
|
131
121
|
// sends a level up front for catalog-marked models and retries on the 400
|
|
132
122
|
// for the rest. Fires on a 5-min idle timer, so no cross-call memo.
|
|
@@ -146,11 +136,7 @@ export default function recapExtension(pi: ExtensionAPI) {
|
|
|
146
136
|
);
|
|
147
137
|
}, undefined, (usage) => recordUsage(pi, "recap", usage));
|
|
148
138
|
if (controller.signal.aborted) return;
|
|
149
|
-
const content = result.content
|
|
150
|
-
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
151
|
-
.map((block) => block.text)
|
|
152
|
-
.join("\n")
|
|
153
|
-
.trim();
|
|
139
|
+
const content = answerText(result.content);
|
|
154
140
|
if (!content) return;
|
|
155
141
|
scheduler.markFired();
|
|
156
142
|
pi.appendEntry<RecapData>(ENTRY_TYPE, { content });
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* prepends is omitted here (kept decoupled from One Code's memory extension).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { trimToTurnBoundary } from "../lib/side-call.ts";
|
|
10
|
+
|
|
9
11
|
/** CC's REFERENCE_MARK (figures.ts) — the away-summary/recap marker. */
|
|
10
12
|
export const REFERENCE_MARK = "※";
|
|
11
13
|
|
|
@@ -38,7 +40,5 @@ export function recapLine(content: string): string {
|
|
|
38
40
|
* `{ role }` so it is testable with plain objects.
|
|
39
41
|
*/
|
|
40
42
|
export function recentForRecap<T extends { role: string }>(messages: readonly T[], window = RECENT_MESSAGE_WINDOW): T[] {
|
|
41
|
-
|
|
42
|
-
const start = tail.findIndex((message) => message.role === "user" || message.role === "assistant");
|
|
43
|
-
return start === -1 ? [] : tail.slice(start);
|
|
43
|
+
return trimToTurnBoundary(messages.slice(-window));
|
|
44
44
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-title extension — Claude Code's automatic session name.
|
|
3
|
+
*
|
|
4
|
+
* CC names every session once, from the first real user message, with one
|
|
5
|
+
* small-model call (`utils/sessionTitle.ts` `generateSessionTitle`; the 2.1.278
|
|
6
|
+
* request is `captures/session_name.json` — findings §14), and shows the name
|
|
7
|
+
* in the terminal title and the session list. pi has the slot for it —
|
|
8
|
+
* `pi.setSessionName()`, what `/name` sets: the session picker shows it instead
|
|
9
|
+
* of the first message, and `session_info_changed` lets the branding extension
|
|
10
|
+
* retitle the terminal — but never fills it by itself.
|
|
11
|
+
*
|
|
12
|
+
* Shape (title.ts holds the strings): the cheapest capable contained model
|
|
13
|
+
* (pickEconomicalContainedModel — CC's Haiku analog, the same pick the recap
|
|
14
|
+
* makes), CC's naming instruction as the system prompt, the first prose
|
|
15
|
+
* wrapped in `<session>` tags, no tools, thinking off, ~a line of output.
|
|
16
|
+
* Fire-and-forget: the turn never waits for it, a failure names nothing and
|
|
17
|
+
* does not retry (CC retries on the next message when Haiku returns nothing;
|
|
18
|
+
* here one attempt per session keeps a broken provider from being hit on
|
|
19
|
+
* every prompt), and a user's own `/name` is never overwritten. Skipped in
|
|
20
|
+
* one-shot modes (`-p`, `--mode json`: the process exits at settle, a title
|
|
21
|
+
* has nowhere to show) and under CC_SESSION_TITLE=0.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
25
|
+
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
26
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import { withReasoningFallback } from "../lib/model-policy.ts";
|
|
28
|
+
import { pickEconomicalContainedModel } from "../lib/model-tier.ts";
|
|
29
|
+
import { sessionOutlivesTurn } from "../lib/notifications.ts";
|
|
30
|
+
import { recordUsage } from "../lib/usage-bus.ts";
|
|
31
|
+
import { firstUserText, isNameableText, parseTitle, SESSION_TITLE_PROMPT, sessionTitleInput } from "./title.ts";
|
|
32
|
+
|
|
33
|
+
const TITLE_MAX_TOKENS = 128; // a noun phrase, with room for a JSON wrapper
|
|
34
|
+
const TITLE_TIMEOUT_MS = 30_000;
|
|
35
|
+
|
|
36
|
+
export default function sessionTitleExtension(pi: ExtensionAPI) {
|
|
37
|
+
/** One attempt per session, like CC's `haikuTitleAttemptedRef`. */
|
|
38
|
+
let attempted = false;
|
|
39
|
+
let inFlight: AbortController | undefined;
|
|
40
|
+
|
|
41
|
+
const reset = () => {
|
|
42
|
+
attempted = false;
|
|
43
|
+
inFlight?.abort();
|
|
44
|
+
inFlight = undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// A new session (startup, /clear, a resume, a /tree switch) is a new
|
|
48
|
+
// naming opportunity; an in-flight request belonged to the old one.
|
|
49
|
+
pi.on("session_start", reset);
|
|
50
|
+
pi.on("session_shutdown", reset);
|
|
51
|
+
|
|
52
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
53
|
+
if (attempted || process.env.CC_SESSION_TITLE === "0") return;
|
|
54
|
+
if (!sessionOutlivesTurn(ctx.mode)) return;
|
|
55
|
+
if (pi.getSessionName()) {
|
|
56
|
+
attempted = true; // the user (or a resume) named it already
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// CC names after the FIRST user message; on a fresh session that is the
|
|
60
|
+
// prompt being submitted (not yet in the entries when this fires). That
|
|
61
|
+
// message never changes, so it is examined once: a first message that is
|
|
62
|
+
// not prose (a breadcrumb, a slash command sent as text) leaves the
|
|
63
|
+
// session unnamed rather than re-scanning the entries every turn.
|
|
64
|
+
attempted = true;
|
|
65
|
+
const text = firstUserText(ctx.sessionManager.getEntries() as never) ?? event.prompt;
|
|
66
|
+
if (!isNameableText(text)) return;
|
|
67
|
+
void generate(text, ctx);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
async function generate(text: string, ctx: ExtensionContext) {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
inFlight = controller;
|
|
73
|
+
try {
|
|
74
|
+
const model = ctx.model;
|
|
75
|
+
if (!model) return;
|
|
76
|
+
const choice = pickEconomicalContainedModel(ctx.modelRegistry.getAvailable(), model);
|
|
77
|
+
if (!choice) return;
|
|
78
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(choice.model);
|
|
79
|
+
if (!auth.ok) return;
|
|
80
|
+
const baseUrl = (auth as { baseUrl?: string }).baseUrl;
|
|
81
|
+
const result = await withReasoningFallback(
|
|
82
|
+
choice.model,
|
|
83
|
+
(reasoning) => {
|
|
84
|
+
const timeout = AbortSignal.timeout(TITLE_TIMEOUT_MS);
|
|
85
|
+
return completeSimple(
|
|
86
|
+
baseUrl ? ({ ...choice.model, baseUrl } as Model<Api>) : choice.model,
|
|
87
|
+
{
|
|
88
|
+
systemPrompt: SESSION_TITLE_PROMPT,
|
|
89
|
+
messages: [{ role: "user", content: sessionTitleInput(text), timestamp: Date.now() }],
|
|
90
|
+
tools: [],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
apiKey: auth.apiKey,
|
|
94
|
+
headers: auth.headers,
|
|
95
|
+
env: auth.env,
|
|
96
|
+
signal: AbortSignal.any([controller.signal, timeout]),
|
|
97
|
+
maxTokens: TITLE_MAX_TOKENS,
|
|
98
|
+
...(reasoning ? { reasoning } : {}),
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
undefined,
|
|
103
|
+
(usage) => recordUsage(pi, "session-title", usage),
|
|
104
|
+
);
|
|
105
|
+
if (controller.signal.aborted) return;
|
|
106
|
+
const reply = result.content
|
|
107
|
+
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
108
|
+
.map((block) => block.text)
|
|
109
|
+
.join("\n");
|
|
110
|
+
const title = parseTitle(reply);
|
|
111
|
+
// The user may have typed /name while the request was out; theirs wins.
|
|
112
|
+
if (!title || pi.getSessionName()) return;
|
|
113
|
+
pi.setSessionName(title);
|
|
114
|
+
} catch {
|
|
115
|
+
// Best-effort, like CC: an unnamed session is the only consequence.
|
|
116
|
+
} finally {
|
|
117
|
+
if (inFlight === controller) inFlight = undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|