@oh-my-pi/pi-coding-agent 16.1.0 → 16.1.2
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 +36 -1
- package/dist/cli.js +3134 -3158
- package/dist/types/cli/bench-cli.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +28 -37
- package/dist/types/lsp/types.d.ts +5 -3
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +12 -0
- package/dist/types/modes/components/cache-invalidation-marker.d.ts +7 -2
- package/dist/types/modes/components/welcome.d.ts +1 -1
- package/dist/types/sdk.d.ts +19 -2
- package/dist/types/session/auth-broker-config.d.ts +33 -6
- package/dist/types/system-prompt.d.ts +5 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/tools/find.d.ts +0 -2
- package/dist/types/tools/search.d.ts +3 -3
- package/package.json +12 -12
- package/scripts/measure-prompt-tokens.ts +63 -0
- package/src/cli/bench-cli.ts +64 -3
- package/src/cli/startup-cwd.ts +3 -13
- package/src/config/settings-schema.ts +34 -37
- package/src/config/settings.ts +40 -0
- package/src/cursor.ts +1 -1
- package/src/debug/raw-sse-buffer.ts +31 -10
- package/src/eval/py/prelude.py +1 -1
- package/src/export/html/tool-views.generated.js +1 -1
- package/src/extensibility/extensions/runner.ts +8 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +9 -9
- package/src/lsp/types.ts +6 -3
- package/src/main.ts +29 -9
- package/src/modes/components/assistant-message.ts +86 -0
- package/src/modes/components/cache-invalidation-marker.ts +12 -2
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/welcome.ts +86 -8
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/prompts/system/personalities/default.md +8 -16
- package/src/prompts/system/system-prompt.md +101 -115
- package/src/prompts/tools/ast-edit.md +10 -12
- package/src/prompts/tools/ast-grep.md +14 -18
- package/src/prompts/tools/bash.md +19 -21
- package/src/prompts/tools/browser.md +24 -24
- package/src/prompts/tools/checkpoint.md +0 -1
- package/src/prompts/tools/debug.md +11 -15
- package/src/prompts/tools/eval.md +27 -27
- package/src/prompts/tools/find.md +6 -10
- package/src/prompts/tools/github.md +11 -15
- package/src/prompts/tools/goal.md +0 -7
- package/src/prompts/tools/inspect-image.md +0 -1
- package/src/prompts/tools/irc.md +15 -24
- package/src/prompts/tools/job.md +5 -8
- package/src/prompts/tools/learn.md +2 -2
- package/src/prompts/tools/lsp.md +27 -30
- package/src/prompts/tools/manage-skill.md +4 -4
- package/src/prompts/tools/read.md +21 -23
- package/src/prompts/tools/replace.md +0 -1
- package/src/prompts/tools/resolve.md +4 -9
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search.md +8 -10
- package/src/prompts/tools/task.md +33 -38
- package/src/prompts/tools/todo.md +14 -18
- package/src/prompts/tools/web-search.md +0 -4
- package/src/prompts/tools/write.md +1 -1
- package/src/sdk.ts +49 -102
- package/src/session/agent-session.ts +23 -12
- package/src/session/auth-broker-config.ts +36 -76
- package/src/session/session-history-format.ts +1 -1
- package/src/session/session-manager.ts +33 -6
- package/src/system-prompt.ts +28 -8
- package/src/task/executor.ts +57 -0
- package/src/task/index.ts +15 -1
- package/src/tools/browser.ts +1 -1
- package/src/tools/eval.ts +1 -1
- package/src/tools/find.ts +4 -17
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/search.ts +5 -5
package/src/lsp/client.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { applyWorkspaceEdit } from "./edits";
|
|
|
5
5
|
import { getLspmuxCommand, isLspmuxSupported } from "./lspmux";
|
|
6
6
|
import type {
|
|
7
7
|
LspClient,
|
|
8
|
+
LspJsonRpcId,
|
|
8
9
|
LspJsonRpcNotification,
|
|
9
10
|
LspJsonRpcRequest,
|
|
10
11
|
LspJsonRpcResponse,
|
|
@@ -416,7 +417,6 @@ function currentWorkspaceFolders(client: LspClient): Array<{ uri: string; name:
|
|
|
416
417
|
* Handle workspace/workspaceFolders requests from the server.
|
|
417
418
|
*/
|
|
418
419
|
async function handleWorkspaceFoldersRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
|
|
419
|
-
if (typeof message.id !== "number") return;
|
|
420
420
|
await sendResponse(client, message.id, currentWorkspaceFolders(client), "workspace/workspaceFolders");
|
|
421
421
|
}
|
|
422
422
|
|
|
@@ -424,7 +424,6 @@ async function handleWorkspaceFoldersRequest(client: LspClient, message: LspJson
|
|
|
424
424
|
* Handle workspace/configuration requests from the server.
|
|
425
425
|
*/
|
|
426
426
|
async function handleConfigurationRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
|
|
427
|
-
if (typeof message.id !== "number") return;
|
|
428
427
|
const params = message.params as { items?: Array<{ section?: string }> };
|
|
429
428
|
const items = params?.items ?? [];
|
|
430
429
|
const result = items.map(item => {
|
|
@@ -438,7 +437,6 @@ async function handleConfigurationRequest(client: LspClient, message: LspJsonRpc
|
|
|
438
437
|
* Handle workspace/applyEdit requests from the server.
|
|
439
438
|
*/
|
|
440
439
|
async function handleApplyEditRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
|
|
441
|
-
if (typeof message.id !== "number") return;
|
|
442
440
|
const params = message.params as { edit?: WorkspaceEdit };
|
|
443
441
|
if (!params?.edit) {
|
|
444
442
|
await sendResponse(
|
|
@@ -475,13 +473,15 @@ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest
|
|
|
475
473
|
return;
|
|
476
474
|
}
|
|
477
475
|
if (message.method === "window/workDoneProgress/create") {
|
|
478
|
-
// Accept progress token registration from the server
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
476
|
+
// Accept progress token registration from the server.
|
|
477
|
+
await sendResponse(client, message.id, null, message.method);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (message.method === "client/registerCapability" || message.method === "client/unregisterCapability") {
|
|
481
|
+
// Some servers block semantic requests until dynamic registration succeeds.
|
|
482
|
+
await sendResponse(client, message.id, null, message.method);
|
|
482
483
|
return;
|
|
483
484
|
}
|
|
484
|
-
if (typeof message.id !== "number") return;
|
|
485
485
|
await sendResponse(client, message.id, null, message.method, {
|
|
486
486
|
code: -32601,
|
|
487
487
|
message: `Method not found: ${message.method}`,
|
|
@@ -493,7 +493,7 @@ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest
|
|
|
493
493
|
*/
|
|
494
494
|
async function sendResponse(
|
|
495
495
|
client: LspClient,
|
|
496
|
-
id:
|
|
496
|
+
id: LspJsonRpcId,
|
|
497
497
|
result: unknown,
|
|
498
498
|
method: string,
|
|
499
499
|
error?: { code: number; message: string; data?: unknown },
|
package/src/lsp/types.ts
CHANGED
|
@@ -399,7 +399,7 @@ export interface LspClient {
|
|
|
399
399
|
diagnostics: Map<string, PublishedDiagnostics>;
|
|
400
400
|
diagnosticsVersion: number;
|
|
401
401
|
openFiles: Map<string, OpenFile>;
|
|
402
|
-
pendingRequests: Map<number, PendingRequest>;
|
|
402
|
+
pendingRequests: Map<number | string, PendingRequest>;
|
|
403
403
|
messageBuffer: Uint8Array;
|
|
404
404
|
isReading: boolean;
|
|
405
405
|
/** Lifecycle state: "connecting" until initialize completes, then "ready"; "error" on init failure or reader death. */
|
|
@@ -420,16 +420,19 @@ export interface LspClient {
|
|
|
420
420
|
// JSON-RPC Protocol Types
|
|
421
421
|
// =============================================================================
|
|
422
422
|
|
|
423
|
+
/** JSON-RPC request/response identifier accepted by LSP peers. */
|
|
424
|
+
export type LspJsonRpcId = number | string;
|
|
425
|
+
|
|
423
426
|
export interface LspJsonRpcRequest {
|
|
424
427
|
jsonrpc: "2.0";
|
|
425
|
-
id:
|
|
428
|
+
id: LspJsonRpcId;
|
|
426
429
|
method: string;
|
|
427
430
|
params: unknown;
|
|
428
431
|
}
|
|
429
432
|
|
|
430
433
|
export interface LspJsonRpcResponse {
|
|
431
434
|
jsonrpc: "2.0";
|
|
432
|
-
id?:
|
|
435
|
+
id?: LspJsonRpcId;
|
|
433
436
|
result?: unknown;
|
|
434
437
|
error?: { code: number; message: string; data?: unknown };
|
|
435
438
|
}
|
package/src/main.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { EventLoopKeepalive } from "@oh-my-pi/pi-agent-core";
|
|
|
11
11
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
12
12
|
import {
|
|
13
13
|
$env,
|
|
14
|
+
directoryExists,
|
|
14
15
|
getLogPath,
|
|
15
16
|
getProjectDir,
|
|
16
17
|
logger,
|
|
@@ -575,7 +576,11 @@ async function moveMissingCwdSessionIfNeeded(
|
|
|
575
576
|
return { status: "declined" };
|
|
576
577
|
}
|
|
577
578
|
|
|
578
|
-
|
|
579
|
+
// Open anchored at the (now-missing) recorded cwd: `open` otherwise falls back
|
|
580
|
+
// to the launch cwd, which would make the `moveTo` below a no-op whenever the
|
|
581
|
+
// move target equals the current project dir. moveTo never chdirs, so the
|
|
582
|
+
// stale cwd is only a relocation source, not a directory we enter.
|
|
583
|
+
const manager = await SessionManager.open(session.path, sessionDir, undefined, { initialCwd: sourceCwd });
|
|
579
584
|
await manager.moveTo(cwd, sessionDir);
|
|
580
585
|
return { status: "moved", manager };
|
|
581
586
|
}
|
|
@@ -751,6 +756,20 @@ function discoverAppendSystemPromptFile(): string | undefined {
|
|
|
751
756
|
return undefined;
|
|
752
757
|
}
|
|
753
758
|
|
|
759
|
+
/** Apply resolved CLI/discovered prompt files without bypassing system prompt templates. */
|
|
760
|
+
export function applyResolvedSystemPromptInputs(
|
|
761
|
+
options: CreateAgentSessionOptions,
|
|
762
|
+
resolvedSystemPrompt: string | undefined,
|
|
763
|
+
resolvedAppendPrompt: string | undefined,
|
|
764
|
+
): void {
|
|
765
|
+
if (resolvedSystemPrompt) {
|
|
766
|
+
options.customSystemPrompt = resolvedSystemPrompt;
|
|
767
|
+
}
|
|
768
|
+
if (resolvedAppendPrompt) {
|
|
769
|
+
options.appendSystemPrompt = resolvedAppendPrompt;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
754
773
|
async function buildSessionOptions(
|
|
755
774
|
parsed: Args,
|
|
756
775
|
scopedModels: ScopedModel[],
|
|
@@ -875,13 +894,7 @@ async function buildSessionOptions(
|
|
|
875
894
|
// (handled by caller before createAgentSession)
|
|
876
895
|
|
|
877
896
|
// System prompt
|
|
878
|
-
|
|
879
|
-
options.systemPrompt = defaultPrompt => [resolvedSystemPrompt, resolvedAppendPrompt, ...defaultPrompt.slice(1)];
|
|
880
|
-
} else if (resolvedSystemPrompt) {
|
|
881
|
-
options.systemPrompt = defaultPrompt => [resolvedSystemPrompt, ...defaultPrompt.slice(1)];
|
|
882
|
-
} else if (resolvedAppendPrompt) {
|
|
883
|
-
options.systemPrompt = defaultPrompt => [...defaultPrompt, resolvedAppendPrompt];
|
|
884
|
-
}
|
|
897
|
+
applyResolvedSystemPromptInputs(options, resolvedSystemPrompt, resolvedAppendPrompt);
|
|
885
898
|
|
|
886
899
|
// Tools
|
|
887
900
|
if (parsed.noTools) {
|
|
@@ -1141,7 +1154,14 @@ export async function runRootCommand(
|
|
|
1141
1154
|
// Resuming a session from another project: switch the process into that
|
|
1142
1155
|
// project's directory and refresh cwd-derived caches before the session is
|
|
1143
1156
|
// built, so settings discovery, plugins, and capabilities all scope to it.
|
|
1144
|
-
|
|
1157
|
+
// Skip the chdir when the recorded project directory is gone: `setProjectDir`
|
|
1158
|
+
// would throw on the missing path. `SessionManager.open` then falls back to
|
|
1159
|
+
// the launch cwd, so the resumed session simply stays where the user is.
|
|
1160
|
+
if (
|
|
1161
|
+
selected.cwd &&
|
|
1162
|
+
normalizePathForComparison(selected.cwd) !== normalizePathForComparison(getProjectDir()) &&
|
|
1163
|
+
(await directoryExists(selected.cwd))
|
|
1164
|
+
) {
|
|
1145
1165
|
// Let the original (launch-cwd) plugin-root preload settle first so its
|
|
1146
1166
|
// late resolution can't clobber the re-warm we trigger below.
|
|
1147
1167
|
await pluginPreloadPromise.catch(() => {});
|
|
@@ -16,6 +16,59 @@ import { type CacheInvalidation, CacheInvalidationMarkerComponent } from "./cach
|
|
|
16
16
|
*/
|
|
17
17
|
const MAX_TRANSCRIPT_ERROR_LINES = 8;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* A GFM table delimiter row (`| --- | :--: |`, with or without bounding pipes).
|
|
21
|
+
* The header row alone does not render a table — this delimiter is what makes
|
|
22
|
+
* Markdown lay one out, and a streaming table re-aligns its columns as rows
|
|
23
|
+
* arrive. Requires at least one column pipe so a bare thematic break (`---`)
|
|
24
|
+
* does not match.
|
|
25
|
+
*/
|
|
26
|
+
const MARKDOWN_TABLE_DELIMITER = /^ {0,3}\|?(?:[ \t]*:?-+:?[ \t]*\|)+[ \t]*:?-*:?[ \t]*$/;
|
|
27
|
+
|
|
28
|
+
/** Opening or closing fence of a code block: ≥3 backticks/tildes plus info string. */
|
|
29
|
+
const CODE_FENCE_LINE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether `text` currently contains reflowing Markdown whose layout is not yet
|
|
33
|
+
* permanent: an open ` ```mermaid ` fence (the diagram reshapes as source
|
|
34
|
+
* arrives) or a GFM table (columns re-align as rows arrive). Used by
|
|
35
|
+
* {@link AssistantMessageComponent.isTranscriptBlockCommitStable}.
|
|
36
|
+
*
|
|
37
|
+
* Fence-aware: a mermaid block is detected by its opener, and table delimiters
|
|
38
|
+
* inside ordinary fenced code (shell pipes, ASCII separators, doc examples) are
|
|
39
|
+
* ignored so a long streamed code block is never held out of native scrollback.
|
|
40
|
+
* A delimiter counts only directly under a pipe-bearing header row, outside any
|
|
41
|
+
* code fence.
|
|
42
|
+
*/
|
|
43
|
+
function detectLiveReflowingMarkdown(text: string): boolean {
|
|
44
|
+
let fence: string | null = null;
|
|
45
|
+
let prevLine = "";
|
|
46
|
+
for (const line of text.split("\n")) {
|
|
47
|
+
const fenceMatch = CODE_FENCE_LINE.exec(line);
|
|
48
|
+
if (fence !== null) {
|
|
49
|
+
// Inside a code block: only a bare matching closing fence ends it.
|
|
50
|
+
if (
|
|
51
|
+
fenceMatch &&
|
|
52
|
+
fenceMatch[2]!.trim() === "" &&
|
|
53
|
+
fenceMatch[1]![0] === fence[0] &&
|
|
54
|
+
fenceMatch[1]!.length >= fence.length
|
|
55
|
+
) {
|
|
56
|
+
fence = null;
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (fenceMatch) {
|
|
61
|
+
if (/^mermaid\b/.test(fenceMatch[2]!.trim())) return true;
|
|
62
|
+
fence = fenceMatch[1]!;
|
|
63
|
+
prevLine = "";
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (prevLine.includes("|") && MARKDOWN_TABLE_DELIMITER.test(line)) return true;
|
|
67
|
+
prevLine = line;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
19
72
|
/**
|
|
20
73
|
* Frames for the streaming "thinking" pulse rendered in place of a hidden
|
|
21
74
|
* thinking block while the model is still producing it. A single fixed-width
|
|
@@ -36,6 +89,15 @@ export class AssistantMessageComponent extends Container {
|
|
|
36
89
|
#convertedKittyImages = new Map<string, ImageContent>();
|
|
37
90
|
#kittyConversionsInFlight = new Set<string>();
|
|
38
91
|
#transcriptBlockFinalized: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* True while a non-finalized text item carries reflowing Markdown — a
|
|
94
|
+
* ` ```mermaid ` fence or a GFM table — whose layout re-flows every frame as
|
|
95
|
+
* source arrives (a diagram reshaping, a table re-aligning its columns), so
|
|
96
|
+
* no prefix is byte-stable until the message finalizes. See
|
|
97
|
+
* {@link isTranscriptBlockCommitStable}. Recomputed in {@link updateContent}
|
|
98
|
+
* ahead of the fast-path return, so it tracks every stream tick.
|
|
99
|
+
*/
|
|
100
|
+
#hasLiveReflowingMarkdown = false;
|
|
39
101
|
/**
|
|
40
102
|
* When true, the turn-ending `Error: …` line for `stopReason === "error"` is
|
|
41
103
|
* suppressed because the same error is currently shown in the pinned banner
|
|
@@ -192,6 +254,21 @@ export class AssistantMessageComponent extends Container {
|
|
|
192
254
|
return this.#transcriptBlockFinalized;
|
|
193
255
|
}
|
|
194
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Whether this still-live block's scrolled-off rows may be committed to
|
|
259
|
+
* immutable native scrollback (the {@link TranscriptContainer} durable-
|
|
260
|
+
* snapshot path). Reflowing Markdown — a streaming mermaid diagram or a GFM
|
|
261
|
+
* table — re-lays-out its body as source arrives (the diagram reshapes, the
|
|
262
|
+
* table re-aligns its columns), so committing an intermediate layout strands
|
|
263
|
+
* a stale fragment in native scrollback that only a full repaint (Ctrl+L) can
|
|
264
|
+
* clear. While such content is still streaming the block therefore stays
|
|
265
|
+
* wholly in the repaintable live region and commits once, at its final
|
|
266
|
+
* layout, when the turn finalizes.
|
|
267
|
+
*/
|
|
268
|
+
isTranscriptBlockCommitStable(): boolean {
|
|
269
|
+
return this.#transcriptBlockFinalized || !this.#hasLiveReflowingMarkdown;
|
|
270
|
+
}
|
|
271
|
+
|
|
195
272
|
getTranscriptBlockVersion(): number {
|
|
196
273
|
return this.#blockVersion;
|
|
197
274
|
}
|
|
@@ -418,6 +495,15 @@ export class AssistantMessageComponent extends Container {
|
|
|
418
495
|
this.#lastMessage = message;
|
|
419
496
|
this.#lastUpdateTransient = opts?.transient === true;
|
|
420
497
|
|
|
498
|
+
// Streaming reflowing Markdown (a mermaid diagram reshaping, a GFM table
|
|
499
|
+
// re-aligning columns) re-lays-out its body each frame; see
|
|
500
|
+
// isTranscriptBlockCommitStable. Detect it from raw text — a Markdown
|
|
501
|
+
// parser only resolves these once the closing fence / delimiter row
|
|
502
|
+
// arrives, but the stale native-scrollback commits happen mid-stream.
|
|
503
|
+
this.#hasLiveReflowingMarkdown = message.content.some(
|
|
504
|
+
content => content.type === "text" && detectLiveReflowingMarkdown(content.text),
|
|
505
|
+
);
|
|
506
|
+
|
|
421
507
|
// Fast path: reuse Markdown children when shape is stable during streaming
|
|
422
508
|
if (this.#tryFastPathUpdate(message)) return;
|
|
423
509
|
|
|
@@ -25,8 +25,13 @@ export interface CacheInvalidation {
|
|
|
25
25
|
* request reads nothing from cache and re-pays for the whole prompt. We detect
|
|
26
26
|
* that as: the previous turn cached a meaningful prefix, yet this turn's
|
|
27
27
|
* `cacheRead` collapsed to zero while it still reprocessed a non-trivial prompt.
|
|
28
|
-
* Returns `undefined` (no marker) for the first turn, tiny contexts,
|
|
29
|
-
* that reused any cache
|
|
28
|
+
* Returns `undefined` (no marker) for the first turn, tiny contexts, turns
|
|
29
|
+
* that reused any cache, and — crucially — turns on providers with *implicit*
|
|
30
|
+
* best-effort caching. Only an explicit, prefix-controlled cache (Anthropic /
|
|
31
|
+
* Bedrock `cache_control`) re-creates the prefix on a cold turn (`cacheWrite >
|
|
32
|
+
* 0`); implicit caches (Google / OpenAI / Fireworks) report `cacheWrite: 0` and
|
|
33
|
+
* drop `cacheRead` to zero intermittently as routine propagation noise that
|
|
34
|
+
* self-heals the next turn, so flagging it would be a false positive.
|
|
30
35
|
*/
|
|
31
36
|
export function detectCacheInvalidation(prev: Usage | undefined, current: Usage): CacheInvalidation | undefined {
|
|
32
37
|
if (!prev) return undefined;
|
|
@@ -34,6 +39,11 @@ export function detectCacheInvalidation(prev: Usage | undefined, current: Usage)
|
|
|
34
39
|
if (prevFootprint < MIN_CACHE_FOOTPRINT) return undefined;
|
|
35
40
|
// Any cache reuse this turn means the prefix survived (at least partly).
|
|
36
41
|
if (current.cacheRead > 0) return undefined;
|
|
42
|
+
// Only an explicit, prefix-controlled cache re-creates the prefix on a cold
|
|
43
|
+
// turn — Anthropic/Bedrock report that as `cacheWrite`. Implicit best-effort
|
|
44
|
+
// caches (Google/OpenAI/Fireworks) report `cacheWrite: 0` and drop `cacheRead`
|
|
45
|
+
// to zero intermittently as propagation noise, not a real invalidation.
|
|
46
|
+
if (current.cacheWrite <= 0) return undefined;
|
|
37
47
|
const reprocessedTokens = current.cacheWrite + current.input;
|
|
38
48
|
if (reprocessedTokens < MIN_CACHE_FOOTPRINT) return undefined;
|
|
39
49
|
return { reprocessedTokens };
|
|
@@ -76,6 +76,13 @@ export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef
|
|
|
76
76
|
|
|
77
77
|
const CONDITIONS: Record<string, () => boolean> = {
|
|
78
78
|
hasImageProtocol: () => !!TERMINAL.imageProtocol,
|
|
79
|
+
advisorEnabled: () => {
|
|
80
|
+
try {
|
|
81
|
+
return Settings.instance.get("advisor.enabled") === true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
79
86
|
hindsightActive: () => {
|
|
80
87
|
try {
|
|
81
88
|
return Settings.instance.get("memory.backend") === "hindsight";
|
|
@@ -20,4 +20,5 @@ Press ctrl+r to search your prompt history and reuse a past message
|
|
|
20
20
|
Pair up live: `/collab` shares your session through an end-to-end encrypted relay link — a teammate runs `/join <link>` to watch tool calls stream and prompt the agent from their own omp
|
|
21
21
|
Press ← ← to drill into a running or finished agent and inspect its tool calls and transcript
|
|
22
22
|
Hit a Codex rate limit? `/usage reset` spends a saved reset credit to immediately restore your quota
|
|
23
|
-
No native tool_calling? Inference provider botches parsing them? `PI_DIALECT=glm|kimi|anthropic…` rolls it locally for them!
|
|
23
|
+
No native tool_calling? Inference provider botches parsing them? `PI_DIALECT=glm|kimi|anthropic…` rolls it locally for them!
|
|
24
|
+
Turn on `/advisor` to attach a second model that reviews every turn and quietly injects advice [NEW]
|
|
@@ -29,16 +29,73 @@ export const WELCOME_SESSION_SLOTS = 4;
|
|
|
29
29
|
*/
|
|
30
30
|
export const WELCOME_LSP_SLOTS = 4;
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/** Trailing marker that flags a tip as a "what's new" callout. Stripped before
|
|
33
|
+
* wrapping (with any preceding whitespace) and replaced by {@link NEW_TAG_TEXT}
|
|
34
|
+
* painted as a shimmering rainbow. Non-global so `.test` stays stateless. */
|
|
35
|
+
const NEW_TIP_MARKER = /\s*\[NEW\]\s*$/;
|
|
36
|
+
|
|
37
|
+
/** Visible text rendered in place of {@link NEW_TIP_MARKER}. */
|
|
38
|
+
const NEW_TAG_TEXT = "NEW!";
|
|
39
|
+
|
|
40
|
+
/** Milliseconds for one full hue rotation of the rainbow "NEW!" tag. */
|
|
41
|
+
const NEW_GLOW_PERIOD_MS = 1500;
|
|
42
|
+
|
|
43
|
+
/** Selection weight for "[NEW]" tips; ordinary tips weigh 1, so a freshly added
|
|
44
|
+
* affordance surfaces this many times as often. */
|
|
45
|
+
const NEW_TIP_WEIGHT = 4;
|
|
46
|
+
|
|
47
|
+
/** Per-tip selection weights, parallel to {@link TIPS}. */
|
|
48
|
+
const TIP_WEIGHTS: readonly number[] = TIPS.map(tip => (NEW_TIP_MARKER.test(tip) ? NEW_TIP_WEIGHT : 1));
|
|
49
|
+
const TIP_WEIGHT_TOTAL = TIP_WEIGHTS.reduce((sum, weight) => sum + weight, 0);
|
|
50
|
+
|
|
51
|
+
/** Pick a tip at random, biased toward "[NEW]" tips by {@link NEW_TIP_WEIGHT}.
|
|
52
|
+
* Returns "" when no tips are embedded. */
|
|
53
|
+
function pickWeightedTip(): string {
|
|
54
|
+
if (TIPS.length === 0) return "";
|
|
55
|
+
let r = Math.random() * TIP_WEIGHT_TOTAL;
|
|
56
|
+
for (let i = 0; i < TIPS.length; i++) {
|
|
57
|
+
r -= TIP_WEIGHTS[i] ?? 1;
|
|
58
|
+
if (r < 0) return TIPS[i] ?? "";
|
|
59
|
+
}
|
|
60
|
+
return TIPS[TIPS.length - 1] ?? "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type ColorEncoding = "ansi-16m" | "ansi-256";
|
|
64
|
+
|
|
65
|
+
/** Paint each glyph of {@link NEW_TAG_TEXT} on a moving HSL rainbow. `phase`
|
|
66
|
+
* rotates the hue offset cyclically; successive renders with increasing phase
|
|
67
|
+
* shimmer, while a fixed phase yields a still rainbow. */
|
|
68
|
+
function renderNewTag(phase: number, encoding: ColorEncoding): string {
|
|
69
|
+
const bold = "\x1b[1m";
|
|
70
|
+
const reset = "\x1b[0m";
|
|
71
|
+
const wrapped = ((phase % 1) + 1) % 1;
|
|
72
|
+
const chars = [...NEW_TAG_TEXT];
|
|
73
|
+
let out = bold;
|
|
74
|
+
let prev = "";
|
|
75
|
+
for (let i = 0; i < chars.length; i++) {
|
|
76
|
+
const hue = Math.round(((i / chars.length + wrapped) % 1) * 360);
|
|
77
|
+
const color = Bun.color(`hsl(${hue}, 95%, 60%)`, encoding) ?? "";
|
|
78
|
+
if (color !== prev) {
|
|
79
|
+
out += color;
|
|
80
|
+
prev = color;
|
|
81
|
+
}
|
|
82
|
+
out += chars[i];
|
|
83
|
+
}
|
|
84
|
+
return out + reset;
|
|
85
|
+
}
|
|
86
|
+
export function renderWelcomeTip(tip: string, boxWidth: number, phase = 0): string[] {
|
|
33
87
|
const label = "Tip: ";
|
|
34
88
|
const labelWidth = visibleWidth(label);
|
|
35
89
|
const bodyBudget = boxWidth - 1 - labelWidth; // 1 = leading indent
|
|
36
90
|
if (bodyBudget < 8) return [];
|
|
37
91
|
|
|
38
|
-
const
|
|
92
|
+
const isNew = NEW_TIP_MARKER.test(tip);
|
|
93
|
+
const body = isNew ? tip.replace(NEW_TIP_MARKER, "") : tip;
|
|
94
|
+
|
|
95
|
+
const wrappedBody = wrapTextWithAnsi(replaceTabs(body), bodyBudget);
|
|
39
96
|
if (wrappedBody.length === 0) return [];
|
|
40
97
|
|
|
41
|
-
const encoding = TERMINAL.trueColor ? "ansi-16m" : "ansi-256";
|
|
98
|
+
const encoding: ColorEncoding = TERMINAL.trueColor ? "ansi-16m" : "ansi-256";
|
|
42
99
|
const purple = Bun.color("#b48cff", encoding) ?? "";
|
|
43
100
|
const lightBlue = Bun.color("#9ccfff", encoding) ?? "";
|
|
44
101
|
const italic = "\x1b[3m";
|
|
@@ -46,11 +103,27 @@ export function renderWelcomeTip(tip: string, boxWidth: number): string[] {
|
|
|
46
103
|
const reset = "\x1b[0m";
|
|
47
104
|
const continuationIndent = padding(labelWidth);
|
|
48
105
|
|
|
49
|
-
|
|
106
|
+
const lines = wrappedBody.map((line, index) =>
|
|
50
107
|
index === 0
|
|
51
|
-
? ` ${italic}${purple}${label}${dim}${lightBlue}${
|
|
52
|
-
: ` ${italic}${continuationIndent}${dim}${lightBlue}${
|
|
108
|
+
? ` ${italic}${purple}${label}${dim}${lightBlue}${line}${reset}`
|
|
109
|
+
: ` ${italic}${continuationIndent}${dim}${lightBlue}${line}${reset}`,
|
|
53
110
|
);
|
|
111
|
+
|
|
112
|
+
if (isNew) {
|
|
113
|
+
// Append the rainbow tag to the final body line when it fits within the
|
|
114
|
+
// box; otherwise drop it onto its own indented continuation line so the
|
|
115
|
+
// styled glyphs never overflow or reflow the wrapped body.
|
|
116
|
+
const tag = renderNewTag(phase, encoding);
|
|
117
|
+
const tagWidth = 1 + visibleWidth(NEW_TAG_TEXT); // 1 = space separator
|
|
118
|
+
const lastLine = lines[lines.length - 1];
|
|
119
|
+
if (lastLine !== undefined && visibleWidth(lastLine) + tagWidth <= boxWidth) {
|
|
120
|
+
lines[lines.length - 1] = `${lastLine} ${tag}`;
|
|
121
|
+
} else {
|
|
122
|
+
lines.push(` ${continuationIndent}${tag}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return lines;
|
|
54
127
|
}
|
|
55
128
|
|
|
56
129
|
export interface RecentSession {
|
|
@@ -89,7 +162,7 @@ export class WelcomeComponent implements Component {
|
|
|
89
162
|
if (theme.getSymbolPreset() === "unicode" && Math.random() < 0.1) {
|
|
90
163
|
this.#selectedTip = "Please use nerdfont 😭.";
|
|
91
164
|
} else {
|
|
92
|
-
this.#selectedTip =
|
|
165
|
+
this.#selectedTip = pickWeightedTip();
|
|
93
166
|
}
|
|
94
167
|
}
|
|
95
168
|
return this.#selectedTip || undefined;
|
|
@@ -327,7 +400,12 @@ export class WelcomeComponent implements Component {
|
|
|
327
400
|
#renderTip(boxWidth: number): string[] {
|
|
328
401
|
const tip = this.tip;
|
|
329
402
|
if (!tip) return [];
|
|
330
|
-
|
|
403
|
+
// A trailing "[NEW]" marker paints an animated rainbow "NEW!" tag. Derive
|
|
404
|
+
// its hue phase from wall-clock time so it shimmers across the welcome
|
|
405
|
+
// intro's re-render frames, then settles into a still rainbow once the box
|
|
406
|
+
// caches its resting frame. Non-"[NEW]" tips ignore the phase entirely.
|
|
407
|
+
const phase = NEW_TIP_MARKER.test(tip) ? performance.now() / NEW_GLOW_PERIOD_MS : 0;
|
|
408
|
+
return renderWelcomeTip(tip, boxWidth, phase);
|
|
331
409
|
}
|
|
332
410
|
|
|
333
411
|
/** Center text within a given width */
|
|
@@ -186,7 +186,7 @@ export class EventController {
|
|
|
186
186
|
}
|
|
187
187
|
#updateWorkingMessageFromIntent(intent: unknown): void {
|
|
188
188
|
if (this.ctx.session.isAborting) return;
|
|
189
|
-
// Streamed JSON can deliver non-string `
|
|
189
|
+
// Streamed JSON can deliver non-string `i` (object, number, boolean) before
|
|
190
190
|
// schema validation; `?.` only guards null/undefined, so guard the type too.
|
|
191
191
|
if (typeof intent !== "string") return;
|
|
192
192
|
const trimmed = intent.trim();
|
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
You are a terse, evidence-first engineer: every sentence carries a fact, a decision, or a risk.
|
|
2
2
|
|
|
3
3
|
# Tone
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- Compress reasoning into facts, constraints, tradeoffs, decisions, and checks. Action-oriented and dense.
|
|
10
|
-
- Do not hide uncertainty: state it briefly at the specific claim, name the tradeoff, and pick the boring/safe option.
|
|
4
|
+
- Terse fragments when clearer. Skip ceremony, hedging, summaries, filler, and marketing language.
|
|
5
|
+
- Don't narrate obvious steps or over-explain basics. Assume a technical reader.
|
|
6
|
+
- Be concrete: exact files, symbols, APIs, state fields, edge cases, verification.
|
|
7
|
+
- Compress reasoning into facts, constraints, tradeoffs, decisions, checks. Lead with the conclusion, then evidence.
|
|
8
|
+
- Don't hide uncertainty: state it at the specific claim, name the tradeoff, pick the boring/safe option.
|
|
11
9
|
- For code, focus on invariants, risks, and verification.
|
|
12
|
-
- Lead with the conclusion, then concrete evidence: changed files and verification.
|
|
13
10
|
|
|
14
11
|
# Reasoning Format
|
|
15
|
-
- Problem: what
|
|
16
|
-
- Decision: what to do & why (concrete facts).
|
|
17
|
-
- Check: what can break & how to verify result.
|
|
18
|
-
- Next: the next concrete edit/action.
|
|
12
|
+
- Problem: what's wrong. Decision: what to do & why. Check: what can break & how to verify. Next: the next concrete action.
|
|
19
13
|
|
|
20
14
|
# Succinct Patterns
|
|
21
|
-
- Y →
|
|
22
|
-
- This is safe: Z.
|
|
23
|
-
- Could do A, but B avoids C.
|
|
15
|
+
- Y → need update X. This is safe: Z. Could do A, but B avoids C.
|
|
24
16
|
|
|
25
17
|
# Escalation
|
|
26
|
-
Push back when the plan hides risk or a claim is wrong: name the risk, show
|
|
18
|
+
Push back when the plan hides risk or a claim is wrong: name the risk, show evidence, propose the alternative. Once overruled, execute the user's call without relitigating.
|