@oh-my-pi/pi-coding-agent 16.1.1 → 16.1.3
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 +50 -0
- package/dist/cli.js +3090 -3115
- package/dist/types/cli/bench-cli.d.ts +2 -1
- package/dist/types/config/model-resolver.d.ts +3 -3
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mnemopi/embed-client.d.ts +70 -0
- package/dist/types/mnemopi/embed-protocol.d.ts +52 -0
- package/dist/types/mnemopi/embed-worker.d.ts +12 -0
- package/dist/types/mnemopi/state.d.ts +9 -1
- package/dist/types/modes/components/assistant-message.d.ts +12 -0
- package/dist/types/modes/components/welcome.d.ts +1 -1
- package/dist/types/sdk.d.ts +19 -2
- package/dist/types/session/agent-storage.d.ts +2 -0
- package/dist/types/session/auth-broker-config.d.ts +34 -6
- package/dist/types/session/history-storage.d.ts +1 -1
- 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/image-gen.d.ts +2 -2
- package/dist/types/tools/search.d.ts +3 -3
- package/dist/types/utils/image-loading.d.ts +1 -1
- package/dist/types/utils/ipc.d.ts +22 -0
- package/dist/types/web/search/providers/perplexity-auth.d.ts +37 -0
- 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/cli.ts +8 -0
- package/src/commands/token.ts +52 -33
- package/src/config/append-only-context-mode.ts +45 -0
- package/src/config/model-discovery.ts +3 -0
- package/src/config/model-registry.ts +21 -3
- package/src/config/model-resolver.ts +31 -8
- package/src/config/settings-schema.ts +1 -1
- package/src/cursor.ts +1 -1
- package/src/debug/raw-sse-buffer.ts +31 -10
- package/src/discovery/builtin-rules/ts-no-return-type.md +0 -1
- 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 +24 -0
- package/src/main.ts +29 -9
- package/src/mnemopi/backend.ts +49 -3
- package/src/mnemopi/embed-client.ts +401 -0
- package/src/mnemopi/embed-protocol.ts +35 -0
- package/src/mnemopi/embed-worker.ts +113 -0
- package/src/mnemopi/state.ts +29 -1
- package/src/modes/components/assistant-message.ts +86 -0
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/welcome.ts +87 -9
- package/src/modes/controllers/event-controller.ts +9 -1
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/theme/theme.ts +69 -0
- 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 +53 -102
- package/src/session/agent-session.ts +25 -2
- package/src/session/agent-storage.ts +14 -0
- package/src/session/auth-broker-config.ts +37 -76
- package/src/session/history-storage.ts +13 -1
- package/src/session/session-history-format.ts +1 -1
- package/src/session/session-manager.ts +33 -6
- package/src/stt/asr-client.ts +2 -7
- package/src/system-prompt.ts +28 -8
- package/src/task/executor.ts +57 -0
- package/src/task/index.ts +15 -1
- package/src/tiny/title-client.ts +2 -7
- package/src/tools/browser.ts +1 -1
- package/src/tools/eval.ts +1 -1
- package/src/tools/find.ts +4 -17
- package/src/tools/image-gen.ts +4 -8
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/render-utils.ts +4 -1
- package/src/tools/search.ts +5 -5
- package/src/tts/tts-client.ts +2 -7
- package/src/utils/image-loading.ts +12 -2
- package/src/utils/ipc.ts +38 -0
- package/src/web/search/providers/perplexity-auth.ts +133 -0
- package/src/web/search/providers/perplexity.ts +2 -125
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mnemopi local-embeddings worker. Loaded inside the dedicated subprocess
|
|
3
|
+
* spawned by `embed-client.ts` (re-entered through the agent CLI's hidden
|
|
4
|
+
* `__omp_worker_mnemopi_embed` selector). The whole point of this module is
|
|
5
|
+
* that `loadFastembed()` — and therefore `onnxruntime-node`'s NAPI
|
|
6
|
+
* constructor + finalizer — only ever runs in this child address space. The
|
|
7
|
+
* parent `SIGKILL`s us on shutdown so the destructor that crashes Bun on
|
|
8
|
+
* Windows shutdown (issue #3031, mnemopi sibling of #1606/#1607) never runs
|
|
9
|
+
* in either process.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { StandardEmbeddingModel } from "@oh-my-pi/pi-mnemopi/core";
|
|
13
|
+
import { loadFastembed } from "@oh-my-pi/pi-mnemopi/core/fastembed-runtime";
|
|
14
|
+
import type { MnemopiEmbedModelId, MnemopiEmbedTransport, MnemopiEmbedWorkerInbound } from "./embed-protocol";
|
|
15
|
+
|
|
16
|
+
interface LoadedModel {
|
|
17
|
+
model: MnemopiEmbedModelId;
|
|
18
|
+
cacheDir: string | undefined;
|
|
19
|
+
instance: {
|
|
20
|
+
embed(texts: string[], batchSize?: number): AsyncIterable<number[][]> | Iterable<number[][]>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let loaded: Promise<LoadedModel> | null = null;
|
|
25
|
+
let loadedKey = "";
|
|
26
|
+
|
|
27
|
+
async function loadModel(model: MnemopiEmbedModelId, cacheDir: string | undefined): Promise<LoadedModel> {
|
|
28
|
+
const { FlagEmbedding } = await loadFastembed();
|
|
29
|
+
// Cast: `model` arrives as a string from the parent (resolved by
|
|
30
|
+
// mnemopi's `fastembedModelName`). Cast to the non-CUSTOM overload's
|
|
31
|
+
// argument so TypeScript picks the standard-model branch — the parent
|
|
32
|
+
// only ever passes pre-vetted fast-* identifiers.
|
|
33
|
+
const instance = await FlagEmbedding.init({
|
|
34
|
+
model: model as StandardEmbeddingModel,
|
|
35
|
+
cacheDir,
|
|
36
|
+
showDownloadProgress: false,
|
|
37
|
+
});
|
|
38
|
+
return { model, cacheDir, instance };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureLoaded(model: MnemopiEmbedModelId, cacheDir: string | undefined): Promise<LoadedModel> {
|
|
42
|
+
const key = `${model}\u0000${cacheDir ?? ""}`;
|
|
43
|
+
if (loaded !== null && loadedKey === key) return loaded;
|
|
44
|
+
const loading = loadModel(model, cacheDir).catch(error => {
|
|
45
|
+
// Failed loads must not poison the cache — a retry with the same key
|
|
46
|
+
// should re-attempt the load.
|
|
47
|
+
if (loaded === loading) {
|
|
48
|
+
loaded = null;
|
|
49
|
+
loadedKey = "";
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
});
|
|
53
|
+
loaded = loading;
|
|
54
|
+
loadedKey = key;
|
|
55
|
+
return loading;
|
|
56
|
+
}
|
|
57
|
+
async function handleEmbed(
|
|
58
|
+
transport: MnemopiEmbedTransport,
|
|
59
|
+
message: Extract<MnemopiEmbedWorkerInbound, { type: "embed" }>,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
try {
|
|
62
|
+
// Each `embed` carries the model + cacheDir the wrapper was bound to.
|
|
63
|
+
// `ensureLoaded` is idempotent for the same key, so this is a no-op
|
|
64
|
+
// once the model is in memory — and it transparently re-loads after
|
|
65
|
+
// the parent SIGKILLed the previous subprocess but mnemopi still
|
|
66
|
+
// holds the cached `LocalEmbeddingModel` wrapper from before.
|
|
67
|
+
const { instance } = await ensureLoaded(message.model, message.cacheDir);
|
|
68
|
+
const vectors: number[][] = [];
|
|
69
|
+
const batches = instance.embed([...message.texts], message.batchSize);
|
|
70
|
+
for await (const batch of batches) {
|
|
71
|
+
for (const row of batch) vectors.push(row);
|
|
72
|
+
}
|
|
73
|
+
transport.send({ type: "vectors", id: message.id, vectors });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
transport.send({
|
|
76
|
+
type: "error",
|
|
77
|
+
id: message.id,
|
|
78
|
+
error: error instanceof Error ? error.message : String(error),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleInit(
|
|
84
|
+
transport: MnemopiEmbedTransport,
|
|
85
|
+
message: Extract<MnemopiEmbedWorkerInbound, { type: "init" }>,
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
try {
|
|
88
|
+
await ensureLoaded(message.model, message.cacheDir);
|
|
89
|
+
transport.send({ type: "ready", id: message.id });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
transport.send({
|
|
92
|
+
type: "error",
|
|
93
|
+
id: message.id,
|
|
94
|
+
error: error instanceof Error ? error.message : String(error),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function startMnemopiEmbedWorker(transport: MnemopiEmbedTransport): void {
|
|
100
|
+
transport.onMessage(message => {
|
|
101
|
+
switch (message.type) {
|
|
102
|
+
case "ping":
|
|
103
|
+
transport.send({ type: "pong", id: message.id });
|
|
104
|
+
return;
|
|
105
|
+
case "init":
|
|
106
|
+
void handleInit(transport, message);
|
|
107
|
+
return;
|
|
108
|
+
case "embed":
|
|
109
|
+
void handleEmbed(transport, message);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
package/src/mnemopi/state.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
|
3
3
|
import type * as MnemopiNs from "@oh-my-pi/pi-mnemopi";
|
|
4
4
|
import type { Mnemopi, RecallResult } from "@oh-my-pi/pi-mnemopi";
|
|
5
5
|
import type * as MnemopiCoreNs from "@oh-my-pi/pi-mnemopi/core";
|
|
6
|
+
import type { LocalModelInitializer } from "@oh-my-pi/pi-mnemopi/core";
|
|
6
7
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import {
|
|
8
9
|
composeRecallQuery,
|
|
@@ -13,16 +14,42 @@ import {
|
|
|
13
14
|
import { extractMessages } from "../hindsight/transcript";
|
|
14
15
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
15
16
|
import type { MnemopiBackendConfig, MnemopiScoping } from "./config";
|
|
17
|
+
import { mnemopiEmbedClient } from "./embed-client";
|
|
16
18
|
|
|
17
19
|
// The mnemopi package pulls the embeddings stack; keep it off the CLI startup
|
|
18
20
|
// module graph by loading it lazily at the async boundaries that need it.
|
|
19
21
|
let mnemopiMod: typeof MnemopiNs | undefined;
|
|
20
22
|
let mnemopiCoreMod: typeof MnemopiCoreNs | undefined;
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
// `setLocalModelInitializer` writes a single module-level slot shared by
|
|
25
|
+
// both the root and `/core` re-exports, so install at most once across both
|
|
26
|
+
// loaders. Either entry point is enough to wire up the override.
|
|
27
|
+
let localModelInitializerInstalled = false;
|
|
28
|
+
|
|
29
|
+
function installLocalModelInitializer(setInitializer: (initializer: LocalModelInitializer) => void): void {
|
|
30
|
+
if (localModelInitializerInstalled) return;
|
|
31
|
+
localModelInitializerInstalled = true;
|
|
32
|
+
setInitializer(({ model, cacheDir }) =>
|
|
33
|
+
mnemopiEmbedClient.initialize(model, cacheDir).then(handle => {
|
|
34
|
+
if (handle) return handle;
|
|
35
|
+
throw new Error("mnemopi embed subprocess unavailable");
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Lazily load `@oh-my-pi/pi-mnemopi` (memoized) and route fastembed loads
|
|
42
|
+
* through the dedicated embeddings subprocess. The override is installed once
|
|
43
|
+
* — before any consumer gets the chance to call `embed()` — so
|
|
44
|
+
* `onnxruntime-node`'s NAPI constructor + finalizer never run inside the
|
|
45
|
+
* agent's address space (issue #3031). Test seams that swap the initializer
|
|
46
|
+
* with `setLocalModelInitializerForTests` still win because both go through
|
|
47
|
+
* the same module-level slot.
|
|
48
|
+
*/
|
|
23
49
|
export async function loadMnemopi(): Promise<typeof MnemopiNs> {
|
|
24
50
|
if (!mnemopiMod) {
|
|
25
51
|
mnemopiMod = await import("@oh-my-pi/pi-mnemopi");
|
|
52
|
+
installLocalModelInitializer(mnemopiMod.setLocalModelInitializer);
|
|
26
53
|
}
|
|
27
54
|
return mnemopiMod;
|
|
28
55
|
}
|
|
@@ -31,6 +58,7 @@ export async function loadMnemopi(): Promise<typeof MnemopiNs> {
|
|
|
31
58
|
export async function loadMnemopiCore(): Promise<typeof MnemopiCoreNs> {
|
|
32
59
|
if (!mnemopiCoreMod) {
|
|
33
60
|
mnemopiCoreMod = await import("@oh-my-pi/pi-mnemopi/core");
|
|
61
|
+
installLocalModelInitializer(mnemopiCoreMod.setLocalModelInitializer);
|
|
34
62
|
}
|
|
35
63
|
return mnemopiCoreMod;
|
|
36
64
|
}
|
|
@@ -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
|
|
|
@@ -177,7 +177,7 @@ export class CustomEditor extends Editor {
|
|
|
177
177
|
/** Per-render scratch flag: did any layout line in this render contain a magic
|
|
178
178
|
* keyword that should shimmer? Reset by {@link #scheduleShimmerIfNeeded} each
|
|
179
179
|
* time a frame is queued. */
|
|
180
|
-
#shimmerTimer:
|
|
180
|
+
#shimmerTimer: Timer | undefined;
|
|
181
181
|
/** Repaint hook the host wires once at construction. Called from the shimmer
|
|
182
182
|
* timer to request the next animation frame. Undefined when nobody is
|
|
183
183
|
* listening (tests, headless callers); the timer chain still self-cleans. */
|
|
@@ -179,9 +179,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
179
179
|
#providers: ProviderTabState[] = STATIC_PROVIDER_TABS;
|
|
180
180
|
#activeTabIndex: number = 0;
|
|
181
181
|
#refreshingProviders: Set<string> = new Set();
|
|
182
|
-
#scheduledProviderRefreshes: Map<string,
|
|
182
|
+
#scheduledProviderRefreshes: Map<string, Timer> = new Map();
|
|
183
183
|
#refreshSpinnerFrame: number = 0;
|
|
184
|
-
#refreshSpinnerInterval?:
|
|
184
|
+
#refreshSpinnerInterval?: Timer;
|
|
185
185
|
|
|
186
186
|
// Context menu state
|
|
187
187
|
#isMenuOpen: boolean = false;
|
|
@@ -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 {
|
|
@@ -69,7 +142,7 @@ export interface LspServerInfo {
|
|
|
69
142
|
*/
|
|
70
143
|
export class WelcomeComponent implements Component {
|
|
71
144
|
#animStart: number | null = null;
|
|
72
|
-
#animTimer:
|
|
145
|
+
#animTimer: Timer | null = null;
|
|
73
146
|
#selectedTip: string | undefined;
|
|
74
147
|
// Render cache: the welcome box is the first transcript-area component, so
|
|
75
148
|
// returning a stable array reference keeps the whole frame prefix stable.
|
|
@@ -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 */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { THINKING_LOOP_ERROR_MARKER } from "@oh-my-pi/pi-ai/utils/thinking-loop";
|
|
2
3
|
import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
3
4
|
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
4
5
|
import { extractTextContent } from "../../commit/utils";
|
|
@@ -186,7 +187,7 @@ export class EventController {
|
|
|
186
187
|
}
|
|
187
188
|
#updateWorkingMessageFromIntent(intent: unknown): void {
|
|
188
189
|
if (this.ctx.session.isAborting) return;
|
|
189
|
-
// Streamed JSON can deliver non-string `
|
|
190
|
+
// Streamed JSON can deliver non-string `i` (object, number, boolean) before
|
|
190
191
|
// schema validation; `?.` only guards null/undefined, so guard the type too.
|
|
191
192
|
if (typeof intent !== "string") return;
|
|
192
193
|
const trimmed = intent.trim();
|
|
@@ -1014,6 +1015,13 @@ export class EventController {
|
|
|
1014
1015
|
async #handleAutoRetryStart(event: Extract<AgentSessionEvent, { type: "auto_retry_start" }>): Promise<void> {
|
|
1015
1016
|
this.#stopWorkingLoader();
|
|
1016
1017
|
this.ctx.statusContainer.clear();
|
|
1018
|
+
if (event.errorMessage?.includes(THINKING_LOOP_ERROR_MARKER)) {
|
|
1019
|
+
// The retry path drops the failed assistant from runtime context. Do not
|
|
1020
|
+
// restore its inline Error row; just unpin the fixed-region banner so the
|
|
1021
|
+
// retry UI is the visible state.
|
|
1022
|
+
this.#pinnedErrorComponent = undefined;
|
|
1023
|
+
this.ctx.clearPinnedError();
|
|
1024
|
+
}
|
|
1017
1025
|
const delaySeconds = Math.round(event.delayMs / 1000);
|
|
1018
1026
|
this.ctx.retryLoader = new Loader(
|
|
1019
1027
|
this.ctx.ui,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
theme,
|
|
28
28
|
} from "../../modes/theme/theme";
|
|
29
29
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
30
|
-
import type { ResetCreditRedeemOutcome } from "../../session/auth-storage";
|
|
30
|
+
import type { ResetCreditAccountStatus, ResetCreditRedeemOutcome } from "../../session/auth-storage";
|
|
31
31
|
import type { SessionInfo } from "../../session/session-listing";
|
|
32
32
|
import { SessionManager } from "../../session/session-manager";
|
|
33
33
|
import { FileSessionStorage } from "../../session/session-storage";
|
|
@@ -1161,7 +1161,7 @@ export class SelectorController {
|
|
|
1161
1161
|
async showResetUsageSelector(): Promise<void> {
|
|
1162
1162
|
const session = this.ctx.session;
|
|
1163
1163
|
this.ctx.showStatus("Checking saved rate-limit resets…", { dim: true });
|
|
1164
|
-
let statuses:
|
|
1164
|
+
let statuses: ResetCreditAccountStatus[];
|
|
1165
1165
|
try {
|
|
1166
1166
|
statuses = await session.listResetCredits();
|
|
1167
1167
|
} catch (error) {
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -2743,6 +2743,35 @@ export function highlightCode(code: string, lang?: string, highlightTheme: Theme
|
|
|
2743
2743
|
}
|
|
2744
2744
|
|
|
2745
2745
|
export function getSymbolTheme(): SymbolTheme {
|
|
2746
|
+
// Guard against `theme` being undefined (pre-init or cross-module-instance
|
|
2747
|
+
// plugin calls). Fall back to the ASCII preset so the returned symbols are
|
|
2748
|
+
// usable instead of crashing. See #2998.
|
|
2749
|
+
if (typeof theme === "undefined") {
|
|
2750
|
+
const box = {
|
|
2751
|
+
topLeft: "+",
|
|
2752
|
+
topRight: "+",
|
|
2753
|
+
bottomLeft: "+",
|
|
2754
|
+
bottomRight: "+",
|
|
2755
|
+
horizontal: "-",
|
|
2756
|
+
vertical: "|",
|
|
2757
|
+
cross: "+",
|
|
2758
|
+
teeDown: "+",
|
|
2759
|
+
teeUp: "+",
|
|
2760
|
+
teeLeft: "+",
|
|
2761
|
+
teeRight: "+",
|
|
2762
|
+
};
|
|
2763
|
+
return {
|
|
2764
|
+
cursor: ">",
|
|
2765
|
+
inputCursor: "|",
|
|
2766
|
+
boxRound: box,
|
|
2767
|
+
boxSharp: box,
|
|
2768
|
+
table: box,
|
|
2769
|
+
quoteBorder: "|",
|
|
2770
|
+
hrChar: "-",
|
|
2771
|
+
colorSwatch: "[]",
|
|
2772
|
+
spinnerFrames: ["-", "\\", "|", "/"],
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2746
2775
|
const preset = theme.getSymbolPreset();
|
|
2747
2776
|
|
|
2748
2777
|
return {
|
|
@@ -2808,6 +2837,19 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|
|
2808
2837
|
}
|
|
2809
2838
|
|
|
2810
2839
|
export function getSelectListTheme(): SelectListTheme {
|
|
2840
|
+
// Guard against `theme` being undefined (pre-init or cross-module-instance
|
|
2841
|
+
// plugin calls). See #2998.
|
|
2842
|
+
if (typeof theme === "undefined") {
|
|
2843
|
+
return {
|
|
2844
|
+
selectedPrefix: (text: string) => text,
|
|
2845
|
+
selectedText: (text: string) => text,
|
|
2846
|
+
description: (text: string) => text,
|
|
2847
|
+
scrollInfo: (text: string) => text,
|
|
2848
|
+
noMatch: (text: string) => text,
|
|
2849
|
+
symbols: getSymbolTheme(),
|
|
2850
|
+
hovered: (text: string) => text,
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2811
2853
|
return {
|
|
2812
2854
|
selectedPrefix: (text: string) => theme.fg("accent", text),
|
|
2813
2855
|
selectedText: (text: string) => theme.fg("accent", text),
|
|
@@ -2820,6 +2862,16 @@ export function getSelectListTheme(): SelectListTheme {
|
|
|
2820
2862
|
}
|
|
2821
2863
|
|
|
2822
2864
|
export function getEditorTheme(): EditorTheme {
|
|
2865
|
+
// Guard against `theme` being undefined (pre-init or cross-module-instance
|
|
2866
|
+
// plugin calls). See #2998.
|
|
2867
|
+
if (typeof theme === "undefined") {
|
|
2868
|
+
return {
|
|
2869
|
+
borderColor: (text: string) => text,
|
|
2870
|
+
selectList: getSelectListTheme(),
|
|
2871
|
+
symbols: getSymbolTheme(),
|
|
2872
|
+
hintStyle: (text: string) => text,
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2823
2875
|
return {
|
|
2824
2876
|
borderColor: (text: string) => theme.fg("borderMuted", text),
|
|
2825
2877
|
selectList: getSelectListTheme(),
|
|
@@ -2829,6 +2881,23 @@ export function getEditorTheme(): EditorTheme {
|
|
|
2829
2881
|
}
|
|
2830
2882
|
|
|
2831
2883
|
export function getSettingsListTheme(): SettingsListTheme {
|
|
2884
|
+
// Plugins (e.g. pi-rtk-optimizer) may call this before `initTheme()` assigns
|
|
2885
|
+
// the global `theme`, or from a separate module instance under npm-global
|
|
2886
|
+
// installs where the live binding was never initialized. Fall back to plain
|
|
2887
|
+
// text so the call returns a usable (unstyled) theme instead of crashing with
|
|
2888
|
+
// "undefined is not an object (evaluating 'theme.fg')". See #2998.
|
|
2889
|
+
if (typeof theme === "undefined") {
|
|
2890
|
+
return {
|
|
2891
|
+
label: (text: string) => text,
|
|
2892
|
+
value: (text: string) => text,
|
|
2893
|
+
description: (text: string) => text,
|
|
2894
|
+
cursor: "> ",
|
|
2895
|
+
hint: (text: string) => text,
|
|
2896
|
+
heading: (text: string) => text,
|
|
2897
|
+
section: (text: string) => text,
|
|
2898
|
+
hovered: (text: string) => text,
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2832
2901
|
return {
|
|
2833
2902
|
label: (text: string, selected: boolean, changed: boolean) =>
|
|
2834
2903
|
changed ? theme.fg("statusLineGitDirty", text) : selected ? theme.fg("accent", text) : text,
|
|
@@ -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.
|