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,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure state, key decoding, and rendering for the `/btw` answer overlay — Claude
|
|
3
|
+
* Code's side-question panel: a focused, scrollable view of the answer with the
|
|
4
|
+
* question dim above it. `↑/↓` (and PgUp/PgDn) scroll, `c` copies the answer,
|
|
5
|
+
* `Esc` closes.
|
|
6
|
+
*
|
|
7
|
+
* Kept free of pi imports so the scroll maths and layout are unit-tested; the
|
|
8
|
+
* extension owns the overlay, repaint, and the actual clipboard write. CC's
|
|
9
|
+
* panel also offers `f to fork`; One Code omits it (pi exposes no primitive to
|
|
10
|
+
* branch a session with an injected exchange) — docs/decisions/btw.md.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { panelTopRule, safeThemePaint, truncateLine, wrapPlainText } from "../lib/tui-render.ts";
|
|
14
|
+
|
|
15
|
+
export const BTW_MAX_HEIGHT = 24;
|
|
16
|
+
|
|
17
|
+
/** Key hint shown at the foot of the panel. Every key here is discoverable. */
|
|
18
|
+
export const BTW_HINT = "↑/↓ to scroll · c to copy · Esc to close";
|
|
19
|
+
|
|
20
|
+
/** Most echoed question lines shown in the header, so it can't starve the body. */
|
|
21
|
+
export const MAX_QUESTION_LINES = 4;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The panel's body state. The pure module owns the text for each phase so every
|
|
25
|
+
* appearance (loading, error, answer) is rendered — and tested — here rather
|
|
26
|
+
* than assembled by the extension wiring.
|
|
27
|
+
*/
|
|
28
|
+
export type BtwBody = { kind: "loading" } | { kind: "error"; message: string } | { kind: "answer"; text: string };
|
|
29
|
+
|
|
30
|
+
/** The text shown in the body for a given phase. */
|
|
31
|
+
function bodyText(body: BtwBody): string {
|
|
32
|
+
switch (body.kind) {
|
|
33
|
+
case "loading":
|
|
34
|
+
return "Thinking…";
|
|
35
|
+
case "error":
|
|
36
|
+
return `Side question failed: ${body.message}`;
|
|
37
|
+
case "answer":
|
|
38
|
+
return body.text || "(no answer)";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BtwPanelState {
|
|
43
|
+
/** First body line shown (0 = top). */
|
|
44
|
+
offset: number;
|
|
45
|
+
/** Largest valid offset for the current viewport; render keeps it current. */
|
|
46
|
+
maxOffset: number;
|
|
47
|
+
/** Visible body lines in the current viewport; render keeps it current. */
|
|
48
|
+
viewport: number;
|
|
49
|
+
/** Set for one frame after a copy so the footer can confirm it. */
|
|
50
|
+
copied: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function initialBtwState(): BtwPanelState {
|
|
54
|
+
return { offset: 0, maxOffset: 0, viewport: 1, copied: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type BtwKey = { kind: "up" | "down" | "pageUp" | "pageDown" | "top" | "bottom" | "copy" | "close" };
|
|
58
|
+
|
|
59
|
+
export function decodeBtwKey(data: string): BtwKey | undefined {
|
|
60
|
+
switch (data) {
|
|
61
|
+
case "\x1b[A":
|
|
62
|
+
case "\x1bOA":
|
|
63
|
+
case "\x10": // ctrl+p
|
|
64
|
+
return { kind: "up" };
|
|
65
|
+
case "\x1b[B":
|
|
66
|
+
case "\x1bOB":
|
|
67
|
+
case "\x0e": // ctrl+n
|
|
68
|
+
return { kind: "down" };
|
|
69
|
+
case "\x1b[5~":
|
|
70
|
+
return { kind: "pageUp" };
|
|
71
|
+
case "\x1b[6~":
|
|
72
|
+
return { kind: "pageDown" };
|
|
73
|
+
case "\x1b[H":
|
|
74
|
+
case "\x1bOH":
|
|
75
|
+
return { kind: "top" };
|
|
76
|
+
case "\x1b[F":
|
|
77
|
+
case "\x1bOF":
|
|
78
|
+
return { kind: "bottom" };
|
|
79
|
+
case "c":
|
|
80
|
+
case "C":
|
|
81
|
+
return { kind: "copy" };
|
|
82
|
+
case "\x1b":
|
|
83
|
+
case "\x03": // ctrl+c
|
|
84
|
+
case "q":
|
|
85
|
+
return { kind: "close" };
|
|
86
|
+
default:
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type BtwEffect = { kind: "copy" } | { kind: "close" };
|
|
92
|
+
|
|
93
|
+
/** Page size for PgUp/PgDn: nearly a full viewport, one line of overlap for orientation. */
|
|
94
|
+
function pageStep(viewport: number): number {
|
|
95
|
+
return Math.max(1, viewport - 1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Apply a key to the scroll state. Returns an effect for the extension to run
|
|
100
|
+
* (copy, close) or undefined for a scroll (repaint only). Offsets are clamped to
|
|
101
|
+
* `state.maxOffset`, and pages by `state.viewport` — both set by the most recent
|
|
102
|
+
* render from the real viewport.
|
|
103
|
+
*/
|
|
104
|
+
export function applyBtwKey(state: BtwPanelState, key: BtwKey): BtwEffect | undefined {
|
|
105
|
+
state.copied = false;
|
|
106
|
+
const step = pageStep(state.viewport);
|
|
107
|
+
switch (key.kind) {
|
|
108
|
+
case "up":
|
|
109
|
+
state.offset = Math.max(0, state.offset - 1);
|
|
110
|
+
return undefined;
|
|
111
|
+
case "down":
|
|
112
|
+
state.offset = Math.min(state.maxOffset, state.offset + 1);
|
|
113
|
+
return undefined;
|
|
114
|
+
case "pageUp":
|
|
115
|
+
state.offset = Math.max(0, state.offset - step);
|
|
116
|
+
return undefined;
|
|
117
|
+
case "pageDown":
|
|
118
|
+
state.offset = Math.min(state.maxOffset, state.offset + step);
|
|
119
|
+
return undefined;
|
|
120
|
+
case "top":
|
|
121
|
+
state.offset = 0;
|
|
122
|
+
return undefined;
|
|
123
|
+
case "bottom":
|
|
124
|
+
state.offset = state.maxOffset;
|
|
125
|
+
return undefined;
|
|
126
|
+
case "copy":
|
|
127
|
+
return { kind: "copy" };
|
|
128
|
+
case "close":
|
|
129
|
+
return { kind: "close" };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Render the panel to at most `height` lines. The header (the dim question) and
|
|
135
|
+
* footer (rule + key hint, with a scroll indicator and a copy confirmation) are
|
|
136
|
+
* fixed; the answer scrolls within what remains. Clamps and writes back
|
|
137
|
+
* `state.offset`/`state.maxOffset` so key handling scrolls against the real
|
|
138
|
+
* body length. Every line is width-truncated (pi-tui crashes on an overwide
|
|
139
|
+
* line).
|
|
140
|
+
*/
|
|
141
|
+
export function renderBtwPanel(
|
|
142
|
+
input: { state: BtwPanelState; question: string; body: BtwBody; width: number; height: number },
|
|
143
|
+
theme?: unknown,
|
|
144
|
+
): string[] {
|
|
145
|
+
const { state, question, body, width, height } = input;
|
|
146
|
+
const paint = safeThemePaint(theme);
|
|
147
|
+
const inner = Math.max(1, width);
|
|
148
|
+
|
|
149
|
+
// Cap the echoed question so a long one (a pasted paragraph or URL) cannot
|
|
150
|
+
// grow the header past the panel height and starve the answer of rows; a
|
|
151
|
+
// truncated last line gets an ellipsis. The answer is what scrolls, not the
|
|
152
|
+
// question.
|
|
153
|
+
const questionLines = wrapPlainText(question.trim(), inner);
|
|
154
|
+
const shownQuestion =
|
|
155
|
+
questionLines.length > MAX_QUESTION_LINES
|
|
156
|
+
? [...questionLines.slice(0, MAX_QUESTION_LINES - 1), truncateLine(`${questionLines[MAX_QUESTION_LINES - 1]}…`, inner)]
|
|
157
|
+
: questionLines;
|
|
158
|
+
const header = [
|
|
159
|
+
paint("accent", truncateLine("Side question", inner)),
|
|
160
|
+
...shownQuestion.map((line) => paint("muted", truncateLine(line, inner))),
|
|
161
|
+
panelTopRule(paint, inner),
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const bodyAll = wrapPlainText(bodyText(body), inner);
|
|
165
|
+
const capacity = Math.max(1, height - header.length - 2); // 2 = footer rule + hint
|
|
166
|
+
state.viewport = capacity;
|
|
167
|
+
state.maxOffset = Math.max(0, bodyAll.length - capacity);
|
|
168
|
+
state.offset = Math.min(Math.max(0, state.offset), state.maxOffset);
|
|
169
|
+
|
|
170
|
+
const visible = bodyAll.slice(state.offset, state.offset + capacity).map((line) => truncateLine(line, inner));
|
|
171
|
+
// Pad a short answer so the footer sits at the panel's foot, not mid-screen.
|
|
172
|
+
while (visible.length < capacity) visible.push("");
|
|
173
|
+
|
|
174
|
+
const scrollNote = state.maxOffset > 0 ? ` (${state.offset + 1}-${Math.min(state.offset + capacity, bodyAll.length)} of ${bodyAll.length})` : "";
|
|
175
|
+
const hint = state.copied ? "Copied to clipboard" : `${BTW_HINT}${scrollNote}`;
|
|
176
|
+
const footer = [panelTopRule(paint, inner), paint("muted", truncateLine(hint, inner))];
|
|
177
|
+
|
|
178
|
+
return [...header, ...visible, ...footer];
|
|
179
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure pieces of Claude Code's `/btw` ("by the way") side-question feature: a
|
|
3
|
+
* lightweight, one-off question answered against the current conversation's
|
|
4
|
+
* context while the main agent keeps working. Prompt text is verbatim from CC's
|
|
5
|
+
* capture (captures/btw.json, CC 2.1.278): a `<system-reminder>` that frames the
|
|
6
|
+
* answering model as a separate, tool-less instance, prepended to the user's
|
|
7
|
+
* question as the final user message.
|
|
8
|
+
*
|
|
9
|
+
* Kept free of pi imports so the message assembly and text extraction are
|
|
10
|
+
* unit-tested; the extension owns the model call, the spinner, and the panel.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CC's verbatim side-question reminder (captures/btw.json message 20). It frames
|
|
15
|
+
* the answering instance as a separate, lightweight, tool-less agent so the
|
|
16
|
+
* model answers in one shot from context instead of trying to act. One Code
|
|
17
|
+
* backs this framing for real: it sends name-only tool stubs (never live tools)
|
|
18
|
+
* and does not persist the exchange, so there genuinely is no follow-up turn.
|
|
19
|
+
*/
|
|
20
|
+
export const SIDE_QUESTION_REMINDER = `<system-reminder>This is a side question from the user. You must answer this question directly in a single response.
|
|
21
|
+
|
|
22
|
+
IMPORTANT CONTEXT:
|
|
23
|
+
- You are a separate, lightweight agent spawned to answer this one question
|
|
24
|
+
- The main agent is NOT interrupted - it continues working independently in the background
|
|
25
|
+
- You share the conversation context but are a completely separate instance
|
|
26
|
+
- Do NOT reference being interrupted or what you were "previously doing" - that framing is incorrect
|
|
27
|
+
|
|
28
|
+
CRITICAL CONSTRAINTS:
|
|
29
|
+
- You have NO tools available - you cannot read files, run commands, search, or take any actions
|
|
30
|
+
- Do NOT write tool calls or tool output as text (for example invoke or function_calls XML blocks) - nothing you write here is executed; if answering would need reading files, running commands, or searching, say that can't be checked from a side question and suggest asking in the main conversation
|
|
31
|
+
- This is a one-off response - there will be no follow-up turns
|
|
32
|
+
- You can ONLY provide information based on what you already know from the conversation context
|
|
33
|
+
- NEVER say things like "Let me try...", "I'll now...", "Let me check...", or promise to take any action
|
|
34
|
+
- If you don't know the answer, say so - do not offer to look it up or investigate
|
|
35
|
+
|
|
36
|
+
Simply answer the question with the information you have.</system-reminder>`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The final user message a `/btw` call sends: CC's reminder, a blank line, then
|
|
40
|
+
* the user's question verbatim (captures/btw.json message 20 is exactly this
|
|
41
|
+
* string). The question is trimmed of surrounding whitespace only.
|
|
42
|
+
*/
|
|
43
|
+
export function sideQuestionMessage(question: string): string {
|
|
44
|
+
return `${SIDE_QUESTION_REMINDER}\n\n${question.trim()}`;
|
|
45
|
+
}
|
|
@@ -13,9 +13,20 @@
|
|
|
13
13
|
* a clean context — which is what a `/clear` habit wants. Like `/exit`→`/quit`,
|
|
14
14
|
* we cannot fold this into pi's `/new` palette entry (pi has no command
|
|
15
15
|
* aliases), so `/clear` and `/new` are two entries that both reset.
|
|
16
|
+
*
|
|
17
|
+
* Claude Code tells the model the conversation was cleared: the new
|
|
18
|
+
* conversation opens with the `/clear` breadcrumb (caveat, command, empty
|
|
19
|
+
* stdout — lib/local-command.ts). It cannot be queued from the handler: pi's
|
|
20
|
+
* `newSession()` replaces the whole runtime (a `session_shutdown` with reason
|
|
21
|
+
* "new" on the old extension runner, every factory run again — findings §8),
|
|
22
|
+
* so a reminder queued before the call dies with the old queue. The NEW
|
|
23
|
+
* instance announces it instead, from its `session_start` with reason "new" —
|
|
24
|
+
* which is also what pi's own `/new` and an RPC `new_session` produce, and
|
|
25
|
+
* they are the same user action as far as the model is concerned.
|
|
16
26
|
*/
|
|
17
27
|
|
|
18
28
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import { announceLocalCommand } from "../lib/local-command.ts";
|
|
19
30
|
|
|
20
31
|
export default function clearExtension(pi: ExtensionAPI) {
|
|
21
32
|
pi.registerCommand("clear", {
|
|
@@ -24,4 +35,8 @@ export default function clearExtension(pi: ExtensionAPI) {
|
|
|
24
35
|
await ctx.newSession();
|
|
25
36
|
},
|
|
26
37
|
});
|
|
38
|
+
|
|
39
|
+
pi.on("session_start", (event) => {
|
|
40
|
+
if ((event as { reason?: string }).reason === "new") announceLocalCommand(pi, { name: "clear" });
|
|
41
|
+
});
|
|
27
42
|
}
|
|
@@ -27,16 +27,19 @@
|
|
|
27
27
|
*
|
|
28
28
|
* Both decisions ask pi-ai's `clampMaxTokensToContext` — the very function
|
|
29
29
|
* `completeSimple` applies to the request — rather than re-deriving its
|
|
30
|
-
* arithmetic, so they can never disagree with the clamp they predict.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* arithmetic, so they can never disagree with the clamp they predict. They
|
|
31
|
+
* `normalizeContext` the request first, exactly as `completeSimple` does, so the
|
|
32
|
+
* prompt and tools are counted (pi 0.86.1 carries them in a leading system
|
|
33
|
+
* message, not separate `Context` fields). One trap in that estimator: it
|
|
34
|
+
* anchors on the last assistant message's `usage` total when there is one
|
|
35
|
+
* (`withoutUsage`).
|
|
33
36
|
*/
|
|
34
37
|
|
|
35
38
|
import { type Api, type Context, contentText, type Message, type Model } from "@earendil-works/pi-ai";
|
|
36
39
|
import { PREVIEW_BYTES } from "../lib/persisted-output.ts";
|
|
37
40
|
// Vendored from pi-ai: importing its deep subpaths directly breaks under the
|
|
38
41
|
// bundled app's library loader (distribution review 2026-09-09, H1).
|
|
39
|
-
import { clampMaxTokensToContext, estimateMessageTokens } from "../lib/pi-ai-estimate.ts";
|
|
42
|
+
import { clampMaxTokensToContext, estimateMessageTokens, normalizeContext } from "../lib/pi-ai-estimate.ts";
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
45
|
* The least output room a replay must leave for the summary. Below it the
|
|
@@ -60,7 +63,7 @@ export const CLEARED_RESULT_HEAD_CHARS = PREVIEW_BYTES;
|
|
|
60
63
|
*/
|
|
61
64
|
export function replayFits(reason: "manual" | "threshold" | "overflow", model: Model<Api>, request: Context, maxTokens: number): boolean {
|
|
62
65
|
if (reason === "overflow") return false;
|
|
63
|
-
return clampMaxTokensToContext(model, request, maxTokens) >= MIN_REPLAY_SUMMARY_TOKENS;
|
|
66
|
+
return clampMaxTokensToContext(model, normalizeContext(request), maxTokens) >= MIN_REPLAY_SUMMARY_TOKENS;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
/**
|
|
@@ -80,7 +83,7 @@ export function fitToBudget(model: Model<Api>, request: Context, maxTokens: numb
|
|
|
80
83
|
|
|
81
84
|
let cleared = 0;
|
|
82
85
|
for (const { index } of candidates) {
|
|
83
|
-
if (clampMaxTokensToContext(model, fitted, maxTokens) >= STANDALONE_SUMMARY_TOKENS) break;
|
|
86
|
+
if (clampMaxTokensToContext(model, normalizeContext(fitted), maxTokens) >= STANDALONE_SUMMARY_TOKENS) break;
|
|
84
87
|
messages[index] = clearToolResult(messages[index]);
|
|
85
88
|
cleared++;
|
|
86
89
|
}
|
|
@@ -17,7 +17,10 @@ import { readJsonFile } from "../lib/atomic-write.ts";
|
|
|
17
17
|
import { oneCodeSettingsPath } from "../lib/one-code-settings.ts";
|
|
18
18
|
import { webSearchSettingsFrom } from "../web/backends.ts";
|
|
19
19
|
import { collectCompat, importedConfigSection, mcpSection } from "./compat.ts";
|
|
20
|
-
import { checkDependencies, dependenciesSection } from "./dependencies.ts";
|
|
20
|
+
import { checkDependencies, dependenciesSection, type ShellsInput } from "./dependencies.ts";
|
|
21
|
+
import { readSettingsEnv } from "../lib/claude-settings.ts";
|
|
22
|
+
import { resolveBashSpawn, resolvePowerShellSpawn } from "../lib/shell-spawn.ts";
|
|
23
|
+
import { shellToolPolicy } from "../powershell/policy.ts";
|
|
21
24
|
import { collectModelFacts, modelsSection } from "./models.ts";
|
|
22
25
|
import { computePresets, presetsSection } from "./presets.ts";
|
|
23
26
|
import {
|
|
@@ -181,7 +184,7 @@ export function buildDoctorReport({ env, registry, session }: BuildInput): Docto
|
|
|
181
184
|
sections.push(mcpSection(compat, session, env.home));
|
|
182
185
|
|
|
183
186
|
const webSearchSettings = webSearchSettingsFrom(readJsonFile<{ webSearch?: unknown }>(oneCodeSettingsPath(env.home, env.env))?.webSearch);
|
|
184
|
-
const deps = checkDependencies({ cwd: env.cwd, env: env.env, platform: env.platform, mcpServers: compat.mcp.servers, sessionModel: session.model, webSearchSettings });
|
|
187
|
+
const deps = checkDependencies({ cwd: env.cwd, env: env.env, platform: env.platform, agentDir: env.agentDir, mcpServers: compat.mcp.servers, sessionModel: session.model, webSearchSettings, shells: resolveShells(env.env, env.home, env.platform) });
|
|
185
188
|
findings.push(...deps.findings);
|
|
186
189
|
sections.push(dependenciesSection(deps));
|
|
187
190
|
|
|
@@ -224,3 +227,20 @@ function summaryText(input: {
|
|
|
224
227
|
export function oneCodeVersion(env: NodeJS.ProcessEnv = process.env): string {
|
|
225
228
|
return env.CC_VERSION ?? extensionVersion();
|
|
226
229
|
}
|
|
230
|
+
|
|
231
|
+
/** The shell tools exactly as a session resolves them (bash/index.ts, powershell/index.ts). */
|
|
232
|
+
export function resolveShells(procEnv: NodeJS.ProcessEnv, home: string, platform: string): ShellsInput {
|
|
233
|
+
const plat = platform as NodeJS.Platform;
|
|
234
|
+
const bash = resolveBashSpawn({ env: procEnv, settingsEnv: readSettingsEnv(home) });
|
|
235
|
+
const powershell = resolvePowerShellSpawn({ platform: plat, env: procEnv });
|
|
236
|
+
const policy = shellToolPolicy({ platform: plat, env: procEnv, bash: !!bash.spawn, powershell: !!powershell });
|
|
237
|
+
return {
|
|
238
|
+
bash: bash.spawn?.shell,
|
|
239
|
+
bashWarning: bash.warning,
|
|
240
|
+
powershell: powershell?.shell,
|
|
241
|
+
powershellWanted: policy.powershellWanted,
|
|
242
|
+
bashExpected: policy.bashExpected,
|
|
243
|
+
primary: policy.primary,
|
|
244
|
+
notices: policy.notices,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
* providers it depends on a third-party key.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { whichOnPath } from "../lib/which.ts";
|
|
16
17
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
17
18
|
import { getProviderKind } from "pi-web-search/src/api.ts";
|
|
18
|
-
import {
|
|
19
|
+
import { installHint, serverInstallHint } from "../lsp/install-hints.ts";
|
|
19
20
|
import { SERVERS } from "../lsp/servers.ts";
|
|
20
21
|
import { typescriptPreflight } from "../lsp/servers.ts";
|
|
21
22
|
import type { McpServer } from "../mcp/config.ts";
|
|
@@ -43,30 +44,6 @@ export interface DependencyReport {
|
|
|
43
44
|
findings: Finding[];
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
/** Minimal `which`: the first PATH entry holding an executable of that name. */
|
|
47
|
-
export function whichOnPath(command: string, env: NodeJS.ProcessEnv, platform: string = process.platform): string | undefined {
|
|
48
|
-
if (!command) return undefined;
|
|
49
|
-
if (command.includes("/") || command.includes("\\")) return isExecutable(command) ? command : undefined;
|
|
50
|
-
const extensions = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
51
|
-
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
52
|
-
if (!dir) continue;
|
|
53
|
-
for (const ext of extensions) {
|
|
54
|
-
const candidate = join(dir, command + ext);
|
|
55
|
-
if (isExecutable(candidate)) return candidate;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isExecutable(path: string): boolean {
|
|
62
|
-
try {
|
|
63
|
-
accessSync(path, constants.X_OK);
|
|
64
|
-
return true;
|
|
65
|
-
} catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
47
|
/** Languages the working directory plausibly contains, by the LSP table's own root markers. */
|
|
71
48
|
export function projectLanguages(cwd: string): string[] {
|
|
72
49
|
const seen = new Map<string, string>(); // command → languageId (one per server binary)
|
|
@@ -86,9 +63,45 @@ export interface DependencyInput {
|
|
|
86
63
|
cwd: string;
|
|
87
64
|
env: NodeJS.ProcessEnv;
|
|
88
65
|
platform?: string;
|
|
66
|
+
/** pi's agent dir; its `bin/` holds the ripgrep pi downloads itself. */
|
|
67
|
+
agentDir?: string;
|
|
89
68
|
mcpServers: McpServer[];
|
|
90
69
|
sessionModel?: Model<Api>;
|
|
91
70
|
webSearchSettings: WebSearchSettings;
|
|
71
|
+
/**
|
|
72
|
+
* The shell tools as the session resolves them (build.ts, from
|
|
73
|
+
* lib/shell-spawn.ts + powershell/policy.ts): which bash, which PowerShell,
|
|
74
|
+
* which one is primary, and the startup notices. On Windows this is the
|
|
75
|
+
* first thing a user asks the doctor — "which shells did you find?" — and
|
|
76
|
+
* the report had no line for it until 2026-09-19 (the Vultr VM session).
|
|
77
|
+
*/
|
|
78
|
+
shells?: ShellsInput;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ShellsInput {
|
|
82
|
+
/** The bash binary the bash tool runs, or undefined when none exists. */
|
|
83
|
+
bash?: string;
|
|
84
|
+
/** An ignored CLAUDE_CODE_GIT_BASH_PATH, in the resolver's words. */
|
|
85
|
+
bashWarning?: string;
|
|
86
|
+
/** The PowerShell executable the powershell tool would run, or undefined. */
|
|
87
|
+
powershell?: string;
|
|
88
|
+
/** From `shellToolPolicy`: the tool is meant to be on (env switch, else Windows). */
|
|
89
|
+
powershellWanted: boolean;
|
|
90
|
+
/** From `shellToolPolicy`: a bash is part of this platform's expected shape (not Windows). */
|
|
91
|
+
bashExpected: boolean;
|
|
92
|
+
primary: "powershell" | "bash" | "none";
|
|
93
|
+
notices: string[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function bashReason(s: ShellsInput): string {
|
|
97
|
+
if (s.primary === "bash") return "the primary shell tool";
|
|
98
|
+
if (s.bash) return "the bash tool, alongside PowerShell";
|
|
99
|
+
return s.bashExpected ? "the bash tool" : "the bash tool (Git for Windows provides one)";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function powershellReason(s: ShellsInput): string {
|
|
103
|
+
if (!s.powershellWanted) return s.bashExpected ? "the powershell tool (off; CLAUDE_CODE_USE_POWERSHELL_TOOL=1 turns it on with a pwsh on PATH)" : "the powershell tool (off: CLAUDE_CODE_USE_POWERSHELL_TOOL=0)";
|
|
104
|
+
return s.primary === "powershell" ? "the primary shell tool" : "the powershell tool";
|
|
92
105
|
}
|
|
93
106
|
|
|
94
107
|
export function checkDependencies(input: DependencyInput): DependencyReport {
|
|
@@ -102,8 +115,27 @@ export function checkDependencies(input: DependencyInput): DependencyReport {
|
|
|
102
115
|
checks.push({ name: "git", found: !!git, path: git, need: "required", reason: "repository detection, auto mode's recoverability check, worktree isolation", hint: "https://git-scm.com/downloads" });
|
|
103
116
|
if (!git) findings.push({ level: "warn", text: "git is not on PATH: worktree isolation and auto mode's git-recoverability check are unavailable.", fix: "Install git and make sure it is on the PATH used to launch One Code." });
|
|
104
117
|
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
// pi downloads ripgrep into <agentDir>/bin on first run when PATH has none
|
|
119
|
+
// and puts that dir on the shell tools' PATH, so a copy there is "found".
|
|
120
|
+
const rgName = platform === "win32" ? "rg.exe" : "rg";
|
|
121
|
+
const bundledRg = input.agentDir ? join(input.agentDir, "bin", rgName) : undefined;
|
|
122
|
+
if (input.shells) {
|
|
123
|
+
const s = input.shells;
|
|
124
|
+
checks.push({ name: "bash", found: !!s.bash, path: s.bash, need: s.bashExpected ? "required" : "optional", reason: bashReason(s), hint: s.bashExpected ? undefined : "https://git-scm.com/download/win" });
|
|
125
|
+
checks.push({ name: "powershell", found: !!s.powershell, path: s.powershell, need: s.powershellWanted ? "required" : "unused", reason: powershellReason(s), hint: "https://aka.ms/powershell" });
|
|
126
|
+
if (s.bashWarning) findings.push({ level: "warn", text: s.bashWarning, fix: "Point CLAUDE_CODE_GIT_BASH_PATH at a bash.exe or sh.exe, or unset it to use Git for Windows' default location." });
|
|
127
|
+
for (const notice of s.notices) {
|
|
128
|
+
const switchSet = notice.includes("CLAUDE_CODE_USE_POWERSHELL_TOOL");
|
|
129
|
+
findings.push({
|
|
130
|
+
level: s.primary === "none" ? "error" : "warn",
|
|
131
|
+
text: notice,
|
|
132
|
+
fix: switchSet ? "Install PowerShell 7 (pwsh) or unset CLAUDE_CODE_USE_POWERSHELL_TOOL." : "Install Git for Windows or PowerShell 7.",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const rg = which("rg") ?? (bundledRg && existsSync(bundledRg) ? bundledRg : undefined);
|
|
138
|
+
checks.push({ name: "rg", found: !!rg, path: rg, need: "optional", reason: "faster code search when the model shells out to ripgrep", hint: installHint("ripgrep", platform) });
|
|
107
139
|
|
|
108
140
|
const languages = projectLanguages(cwd);
|
|
109
141
|
const seenCommands = new Set<string>();
|
|
@@ -119,13 +151,13 @@ export function checkDependencies(input: DependencyInput): DependencyReport {
|
|
|
119
151
|
path: found,
|
|
120
152
|
need: needed ? "project" : "unused",
|
|
121
153
|
reason: `lsp_diagnostics for ${family.join("/")}${needed ? " (this project)" : ""}`,
|
|
122
|
-
hint:
|
|
154
|
+
hint: serverInstallHint(config.command, platform),
|
|
123
155
|
});
|
|
124
156
|
if (needed && !found) {
|
|
125
157
|
findings.push({
|
|
126
158
|
level: "warn",
|
|
127
159
|
text: `${config.command} is not installed, so the model gets no language-server diagnostics after editing ${family[0]} files here.`,
|
|
128
|
-
fix:
|
|
160
|
+
fix: serverInstallHint(config.command, platform) ? `Install it with: ${serverInstallHint(config.command, platform)}` : "Install it and make sure it is on your PATH.",
|
|
129
161
|
});
|
|
130
162
|
}
|
|
131
163
|
if (needed && found && config.command === "typescript-language-server") {
|
|
@@ -22,13 +22,18 @@
|
|
|
22
22
|
* kept in substance. Rationale: docs/decisions/doctor.md.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { modeCycleKey } from "../lib/keys.ts";
|
|
26
|
+
import { UPGRADE_COMMANDS } from "../lib/update-check.mjs";
|
|
27
|
+
|
|
25
28
|
export interface FixPromptInput {
|
|
26
29
|
/** The rendered deterministic report, plain text. */
|
|
27
30
|
reportText: string;
|
|
28
31
|
install: "app" | "pi-package";
|
|
29
32
|
oneCodeVersion: string;
|
|
30
|
-
/** `<agentDir>/sessions`
|
|
33
|
+
/** pi's sessions root, `<agentDir>/sessions` (other projects' directories live here — out of scope). */
|
|
31
34
|
sessionsDir: string;
|
|
35
|
+
/** THIS project's session directory under it — the only transcripts the permission gate lets a session read (Claude Code's own rule). */
|
|
36
|
+
sessionDir: string;
|
|
32
37
|
oneCodeSettingsPath: string;
|
|
33
38
|
oneCodeProjectSettingsPath: string;
|
|
34
39
|
/** True when auto mode is writing `auto-mode-decisions.jsonl` next to the session files. */
|
|
@@ -40,7 +45,10 @@ export function doctorFixPrompt(input: FixPromptInput): string {
|
|
|
40
45
|
input.install === "app"
|
|
41
46
|
? "`npm view @one-ai/one-code version --registry https://registry.npmjs.org/`"
|
|
42
47
|
: "`npm view one-code-extension version --registry https://registry.npmjs.org/`";
|
|
43
|
-
const upgradeCommand =
|
|
48
|
+
const upgradeCommand =
|
|
49
|
+
input.install === "app"
|
|
50
|
+
? `\`${UPGRADE_COMMANDS.npm}\` (or \`${UPGRADE_COMMANDS.brew}\` for a Homebrew install)`
|
|
51
|
+
: `\`${UPGRADE_COMMANDS["pi-package"]}\``;
|
|
44
52
|
|
|
45
53
|
return `# One Code Doctor
|
|
46
54
|
|
|
@@ -66,7 +74,7 @@ Treat every line of that report as measured fact. Your job on those sections is
|
|
|
66
74
|
|
|
67
75
|
## Data sources (all local — the ONLY permitted network access is check 7's read-only version lookup)
|
|
68
76
|
|
|
69
|
-
- **Session transcripts**: pi's JSONL under \`${input.
|
|
77
|
+
- **Session transcripts**: pi's JSONL under \`${input.sessionDir}/*.jsonl\` — this project's sessions, one JSON object per line. That directory is readable without a prompt, like Claude Code's own project directory; the other projects' directories beside it under \`${input.sessionsDir}\` are not in scope and reading them will be gated — stay in this one. Scan the ~50 most recently modified files there and state the window you covered (N sessions over D days). **Read them with single-line read-only commands** — \`Select-String\`, \`Get-Content\`, \`Get-ChildItem\` piped through \`Select-Object\`/\`Sort-Object\`/\`Measure-Object\` in PowerShell, or \`grep\`/\`head\`/\`ls\` piped through \`sort\`/\`head\`/\`wc\` in bash — with no variables, loops, script blocks, \`node -e\` or \`jq\` programs: such a one-liner over this directory runs without a permission review, while a script is reviewed by the auto-mode classifier and may be refused. Several small commands beat one clever script here. Relevant line shapes:
|
|
70
78
|
- Tool calls: \`{"type":"message","message":{"role":"assistant","content":[{"type":"toolCall","name":...,"arguments":...}]}}\`. MCP tools are named \`mcp__<server>__<tool>\` (the server segment is normalized — any character outside \`[a-zA-Z0-9_-]\` becomes \`_\`; plugin servers appear as \`mcp__plugin_<plugin>_<server>__\`). Model-invoked skills are calls to the \`skill\` tool with the skill name in \`arguments.skill\`. Subagent runs are \`Agent\` tool calls.
|
|
71
79
|
- Denied tool calls: a \`toolResult\` whose text starts with "Permission for this action was denied by the auto-mode approval classifier" (auto mode) or "This tool call is denied by the permission rule" (a deny rule). Follow its \`toolCallId\` back to the assistant's \`toolCall\` for the tool name and arguments. ${input.decisionLogEnabled ? "Auto mode is also writing `auto-mode-decisions.jsonl` next to each session file — every gate decision with its verdict; prefer it for check 9." : "Auto mode is not logging its decisions (`autoMode.logDecisions` is off), so transcripts are the only denial record."}
|
|
72
80
|
- There are NO usage counters in One Code: whether a skill, plugin or MCP server was used is answered from transcripts alone.
|
|
@@ -107,7 +115,7 @@ Installed: ${input.oneCodeVersion} (${input.install === "app" ? "the bundled one
|
|
|
107
115
|
|
|
108
116
|
## Check 8 — permission mode (report only)
|
|
109
117
|
|
|
110
|
-
Auto mode is One Code's shipped default. If the report shows a different mode pinned by \`permissions.defaultMode\` in \`~/.claude/settings.json\` or a managed file, say so in one line and leave it — One Code does not edit those files, and the user can change the mode with
|
|
118
|
+
Auto mode is One Code's shipped default. If the report shows a different mode pinned by \`permissions.defaultMode\` in \`~/.claude/settings.json\` or a managed file, say so in one line and leave it — One Code does not edit those files, and the user can change the mode with ${modeCycleKey()} or \`--permission-mode\`. If a repository file pins \`auto\` or \`bypassPermissions\`, the report already explains why it is ignored.
|
|
111
119
|
|
|
112
120
|
## Check 9 — pre-approve frequently denied read-only commands
|
|
113
121
|
|
|
@@ -159,6 +159,7 @@ export default function doctorExtension(pi: ExtensionAPI) {
|
|
|
159
159
|
install: install(),
|
|
160
160
|
oneCodeVersion: oneCodeVersion(),
|
|
161
161
|
sessionsDir: join(getAgentDir(), "sessions"),
|
|
162
|
+
sessionDir: ctx.sessionManager.getSessionDir(),
|
|
162
163
|
oneCodeSettingsPath: oneCodeSettingsPath(home),
|
|
163
164
|
oneCodeProjectSettingsPath: oneCodeProjectSettingsPath(ctx.cwd, home),
|
|
164
165
|
decisionLogEnabled: loadAutoModeConfig(home).logDecisions,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
13
|
+
import { modeCycleKey } from "../lib/keys.ts";
|
|
13
14
|
import { loadAutoModeConfig } from "../auto-mode/config.ts";
|
|
14
15
|
import { classifierCandidates, describeCandidate, type ClassifierNotice } from "../auto-mode/model-select.ts";
|
|
15
16
|
import { ATTRIBUTION, capabilityFloor, configuredCapabilityKey, type FloorRole, type FloorVerdict, KEY_ADVICE, snapshotAgeMs } from "../lib/capability-index.ts";
|
|
@@ -210,7 +211,7 @@ export function modelsSection(facts: ModelFacts, session: SessionView, findings:
|
|
|
210
211
|
if (session.permission) {
|
|
211
212
|
const mode = session.permission.mode;
|
|
212
213
|
const from = session.permission.source ? ` (${session.permission.source})` : "";
|
|
213
|
-
lines.push({ text: `Permission mode: ${mode}${from} —
|
|
214
|
+
lines.push({ text: `Permission mode: ${mode}${from} — ${modeCycleKey()} cycles, /permissions lists the rules`, level: "dim" });
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
if (facts.sessionTier === "tiny") {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
19
|
+
import { tildify } from "../lib/paths.ts";
|
|
19
20
|
import type { McpStatusEvent } from "../lib/mcp-status.ts";
|
|
20
21
|
import { wrapProse } from "../subagents/panel-render.ts";
|
|
21
22
|
import { countNoun } from "../lib/tui-render.ts";
|
|
@@ -183,7 +184,5 @@ export function contextLabel(tokens: number | undefined): string {
|
|
|
183
184
|
|
|
184
185
|
/** `~/…` for anything under home, so paths stay readable at any width. */
|
|
185
186
|
export function shortenHome(path: string, home: string): string {
|
|
186
|
-
|
|
187
|
-
const root = home.endsWith("/") ? home : `${home}/`;
|
|
188
|
-
return path.startsWith(root) ? `~/${path.slice(root.length)}` : path;
|
|
187
|
+
return tildify(path, home);
|
|
189
188
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Newest-release lookup for the doctor's Installation section (pure given a
|
|
3
|
-
* fetch).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* fetch). Shares the app's daily update notice's mechanism
|
|
4
|
+
* (lib/update-check.mjs): the same packument, the same "newest installable
|
|
5
|
+
* version" rule — a day's release age under Homebrew, `dist-tags.latest`
|
|
6
|
+
* otherwise — a short timeout, silent on failure, and skipped under pi's
|
|
7
|
+
* offline switch or ONECODE_NO_UPDATE_CHECK — a diagnostic must never hang on
|
|
8
|
+
* the network or restore egress the user turned off.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import { compareVersions } from "../lib/pi-version.ts";
|
|
12
|
+
import { APP_PACKAGE, EXTENSION_PACKAGE, minReleaseAgeFor, pickAvailableVersion, registryPackumentUrl } from "../lib/update-check.mjs";
|
|
10
13
|
import type { DoctorEnvironment } from "./report.ts";
|
|
11
14
|
|
|
12
|
-
export const APP_REGISTRY_URL =
|
|
13
|
-
export const PACKAGE_REGISTRY_URL =
|
|
15
|
+
export const APP_REGISTRY_URL = registryPackumentUrl(APP_PACKAGE);
|
|
16
|
+
export const PACKAGE_REGISTRY_URL = registryPackumentUrl(EXTENSION_PACKAGE);
|
|
14
17
|
export const LOOKUP_TIMEOUT_MS = 3000;
|
|
15
18
|
|
|
16
19
|
export type LatestLookup = NonNullable<DoctorEnvironment["latest"]>;
|
|
@@ -34,11 +37,16 @@ export async function lookupLatestVersion(input: {
|
|
|
34
37
|
try {
|
|
35
38
|
const response = await fetchImpl(url, { signal: AbortSignal.timeout(input.timeoutMs ?? LOOKUP_TIMEOUT_MS) });
|
|
36
39
|
if (!response.ok) return { status: "unknown", reason: `registry answered ${response.status}` };
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
|
|
40
|
+
const minReleaseAgeMs = minReleaseAgeFor(input.env);
|
|
41
|
+
const body = (await response.json()) as { "dist-tags"?: { latest?: unknown }; time?: unknown };
|
|
42
|
+
const wellFormed = typeof body?.["dist-tags"]?.latest === "string" && (minReleaseAgeMs <= 0 || (typeof body.time === "object" && body.time !== null));
|
|
43
|
+
if (!wellFormed) return { status: "unknown", reason: "unexpected registry payload" };
|
|
44
|
+
const latest = pickAvailableVersion(body, { minReleaseAgeMs });
|
|
45
|
+
// A well-formed packument with nothing old enough is a Homebrew outcome, not a broken registry.
|
|
46
|
+
if (typeof latest !== "string") return { status: "unknown", reason: "no release is a day old yet (Homebrew installs wait that long)" };
|
|
47
|
+
const cmp = compareVersions(latest, input.current);
|
|
48
|
+
if (cmp === undefined) return { status: "unknown", version: latest, reason: "unparseable version" };
|
|
49
|
+
return { status: cmp > 0 ? "behind" : "current", version: latest };
|
|
42
50
|
} catch (error) {
|
|
43
51
|
const message = error instanceof Error ? (error.name === "TimeoutError" ? "timed out" : error.message) : String(error);
|
|
44
52
|
return { status: "unknown", reason: message };
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
ULTRACODE_MODE_CHANNEL,
|
|
43
43
|
ULTRACODE_STATUS_KEY,
|
|
44
44
|
} from "./slider.ts";
|
|
45
|
+
import { registerLocalCommand } from "../lib/local-command.ts";
|
|
45
46
|
|
|
46
47
|
/** The levels pi says the active model can reach; the full ladder when unknown. */
|
|
47
48
|
function supportedLevels(ctx: ExtensionContext): ThinkingLevel[] {
|
|
@@ -193,7 +194,7 @@ export default function effortExtension(pi: ExtensionAPI) {
|
|
|
193
194
|
if (chosen) applyChoice(chosen, ctx);
|
|
194
195
|
};
|
|
195
196
|
|
|
196
|
-
pi
|
|
197
|
+
registerLocalCommand(pi, "effort", {
|
|
197
198
|
description: `Set reasoning effort (shift+tab cycles the plain levels): /effort [${EFFORT_CHOICES.join("|")}] — ultracode is ${ULTRACODE_LEVEL} + workflows`,
|
|
198
199
|
getArgumentCompletions: (prefix) => {
|
|
199
200
|
const matches = acceptedEffortArgs().filter((value) => value.startsWith(prefix.trim().toLowerCase()));
|