@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.6
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 +209 -0
- package/dist/cli.js +3616 -3676
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/extensions/load-errors.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +10 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +9 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/index.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/model.d.ts +3 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +42 -6
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/session/session-paths.d.ts +21 -4
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/tools/xdev.d.ts +5 -3
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +1 -0
- package/dist/types/vibe/runtime.d.ts +23 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/exec/bash-executor.ts +68 -8
- package/src/extensibility/extensions/load-errors.ts +13 -0
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +41 -6
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +25 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/chat-transcript-builder.ts +10 -1
- package/src/modes/components/status-line/component.ts +57 -3
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +23 -122
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/components/usage-row.ts +17 -1
- package/src/modes/controllers/command-controller.ts +41 -1
- package/src/modes/controllers/event-controller.ts +7 -3
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +102 -60
- package/src/modes/noninteractive-dispose.test.ts +14 -1
- package/src/modes/print-mode.ts +81 -9
- package/src/modes/rpc/rpc-client.ts +76 -35
- package/src/modes/rpc/rpc-frame.ts +156 -0
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +11 -4
- package/src/modes/setup-wizard/index.ts +2 -0
- package/src/modes/setup-wizard/scenes/model.ts +134 -0
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +9 -3
- package/src/prompts/system/workflow-notice.md +89 -74
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +484 -188
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/session/session-manager.ts +34 -3
- package/src/session/session-paths.ts +38 -9
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/task/executor.ts +104 -33
- package/src/task/index.ts +46 -9
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/tools/xdev.ts +11 -4
- package/src/tools/yield.ts +4 -1
- package/src/utils/git.ts +237 -0
- package/src/{modes/components/status-line → utils}/token-rate.ts +6 -0
- package/src/vibe/__tests__/token-rate.test.ts +96 -0
- package/src/vibe/runtime.ts +50 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
- /package/dist/types/{modes/components/status-line → utils}/token-rate.d.ts +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { isRecord } from "@oh-my-pi/pi-utils";
|
|
3
|
+
|
|
4
|
+
/** Maximum UTF-8 size of one newline-delimited RPC frame, including the newline. */
|
|
5
|
+
export const MAX_RPC_FRAME_BYTES = 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
interface ShrinkPass {
|
|
8
|
+
stringCap: number;
|
|
9
|
+
arrayLimit: number;
|
|
10
|
+
objectLimit: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SHRINK_PASSES: readonly ShrinkPass[] = [
|
|
14
|
+
{ stringCap: 256 * 1024, arrayLimit: 512, objectLimit: 512 },
|
|
15
|
+
{ stringCap: 64 * 1024, arrayLimit: 256, objectLimit: 256 },
|
|
16
|
+
{ stringCap: 16 * 1024, arrayLimit: 128, objectLimit: 128 },
|
|
17
|
+
{ stringCap: 4 * 1024, arrayLimit: 64, objectLimit: 64 },
|
|
18
|
+
{ stringCap: 1024, arrayLimit: 32, objectLimit: 32 },
|
|
19
|
+
{ stringCap: 256, arrayLimit: 8, objectLimit: 16 },
|
|
20
|
+
{ stringCap: 64, arrayLimit: 1, objectLimit: 8 },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const STRING_ELISION_RESERVE = 80;
|
|
24
|
+
const METADATA_STRING_CAP = 1024;
|
|
25
|
+
|
|
26
|
+
function serializedFrameBytes(json: string): number {
|
|
27
|
+
return Buffer.byteLength(json, "utf8") + 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function shrinkString(value: string, cap: number): string {
|
|
31
|
+
if (value.length <= cap) return value;
|
|
32
|
+
const headLength = Math.max(0, cap - STRING_ELISION_RESERVE);
|
|
33
|
+
return `${value.slice(0, headLength)}\n…[${value.length - headLength} chars elided for RPC frame]`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shrinkValue(value: unknown, pass: ShrinkPass): unknown {
|
|
37
|
+
if (typeof value === "string") return shrinkString(value, pass.stringCap);
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
const keep = Math.min(value.length, pass.arrayLimit);
|
|
40
|
+
const output: unknown[] = new Array(keep + (keep < value.length ? 1 : 0));
|
|
41
|
+
for (let index = 0; index < keep; index++) output[index] = shrinkValue(value[index], pass);
|
|
42
|
+
if (keep < value.length) output[keep] = `…[${value.length - keep} items elided for RPC frame]`;
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
if (isRecord(value)) {
|
|
46
|
+
const entries = Object.entries(value);
|
|
47
|
+
const keep = Math.min(entries.length, pass.objectLimit);
|
|
48
|
+
const output: Record<string, unknown> = {};
|
|
49
|
+
for (let index = 0; index < keep; index++) {
|
|
50
|
+
const [key, item] = entries[index];
|
|
51
|
+
output[key] = shrinkValue(item, pass);
|
|
52
|
+
}
|
|
53
|
+
if (keep < entries.length) output.rpcFrameElidedKeys = entries.length - keep;
|
|
54
|
+
return output;
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function jsonSnapshot(value: unknown): unknown {
|
|
60
|
+
const json = JSON.stringify(value);
|
|
61
|
+
return json === undefined ? undefined : JSON.parse(json);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function encodedMessageSnapshot(encoded: string): { message: unknown } | undefined {
|
|
65
|
+
const frame = JSON.parse(encoded);
|
|
66
|
+
return isRecord(frame) && frame.type === "message_end" && Object.hasOwn(frame, "message")
|
|
67
|
+
? { message: frame.message }
|
|
68
|
+
: undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compactTerminalFrame(
|
|
72
|
+
frame: object,
|
|
73
|
+
streamedMessageCount: number,
|
|
74
|
+
streamedMessages?: readonly unknown[],
|
|
75
|
+
): object {
|
|
76
|
+
if (!isRecord(frame) || frame.type !== "agent_end" || !Array.isArray(frame.messages)) return frame;
|
|
77
|
+
let streamed = Number.isSafeInteger(streamedMessageCount)
|
|
78
|
+
? Math.min(Math.max(0, streamedMessageCount), frame.messages.length)
|
|
79
|
+
: 0;
|
|
80
|
+
if (streamedMessages) {
|
|
81
|
+
streamed = 0;
|
|
82
|
+
const limit = Math.min(streamedMessages.length, frame.messages.length);
|
|
83
|
+
while (
|
|
84
|
+
streamed < limit &&
|
|
85
|
+
isDeepStrictEqual(streamedMessages[streamed], jsonSnapshot(frame.messages[streamed]))
|
|
86
|
+
) {
|
|
87
|
+
streamed++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...frame,
|
|
92
|
+
messages: frame.messages.slice(streamed),
|
|
93
|
+
messageCount: frame.messages.length,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function overflowFrame(frame: object): object {
|
|
98
|
+
if (!isRecord(frame)) return { type: "rpc_frame_error", error: "RPC frame exceeded the transport limit" };
|
|
99
|
+
if (frame.type === "response") {
|
|
100
|
+
return {
|
|
101
|
+
id: typeof frame.id === "string" ? shrinkString(frame.id, METADATA_STRING_CAP) : undefined,
|
|
102
|
+
type: "response",
|
|
103
|
+
command: typeof frame.command === "string" ? shrinkString(frame.command, METADATA_STRING_CAP) : "unknown",
|
|
104
|
+
success: false,
|
|
105
|
+
error: "RPC response exceeded the transport limit",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (frame.type === "agent_end") {
|
|
109
|
+
return {
|
|
110
|
+
type: "agent_end",
|
|
111
|
+
messages: [],
|
|
112
|
+
messageCount: typeof frame.messageCount === "number" ? frame.messageCount : 0,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
type: "rpc_frame_error",
|
|
117
|
+
originalType: typeof frame.type === "string" ? shrinkString(frame.type, METADATA_STRING_CAP) : undefined,
|
|
118
|
+
error: "RPC frame exceeded the transport limit",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Serialize a complete JSONL frame while enforcing the transport byte ceiling. */
|
|
123
|
+
export function encodeRpcFrame(frame: object, streamedMessageCount = 0, streamedMessages?: readonly unknown[]): string {
|
|
124
|
+
let json = JSON.stringify(frame);
|
|
125
|
+
if (serializedFrameBytes(json) <= MAX_RPC_FRAME_BYTES) return `${json}\n`;
|
|
126
|
+
if (isRecord(frame) && frame.type === "response") {
|
|
127
|
+
return `${JSON.stringify(overflowFrame(frame))}\n`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const compacted = compactTerminalFrame(frame, streamedMessageCount, streamedMessages);
|
|
131
|
+
json = JSON.stringify(compacted);
|
|
132
|
+
if (serializedFrameBytes(json) <= MAX_RPC_FRAME_BYTES) return `${json}\n`;
|
|
133
|
+
|
|
134
|
+
for (const pass of SHRINK_PASSES) {
|
|
135
|
+
json = JSON.stringify(shrinkValue(compacted, pass));
|
|
136
|
+
if (serializedFrameBytes(json) <= MAX_RPC_FRAME_BYTES) return `${json}\n`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return `${JSON.stringify(overflowFrame(compacted))}\n`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Stateful encoder that tracks which messages a client has already received. */
|
|
143
|
+
export class RpcFrameEncoder {
|
|
144
|
+
#streamedMessages: unknown[] = [];
|
|
145
|
+
|
|
146
|
+
encode(frame: object): string {
|
|
147
|
+
if (isRecord(frame) && frame.type === "agent_start") this.#streamedMessages = [];
|
|
148
|
+
const encoded = encodeRpcFrame(frame, this.#streamedMessages.length, this.#streamedMessages);
|
|
149
|
+
if (!isRecord(frame)) return encoded;
|
|
150
|
+
if (frame.type === "message_end") {
|
|
151
|
+
const snapshot = encodedMessageSnapshot(encoded);
|
|
152
|
+
if (snapshot) this.#streamedMessages.push(snapshot.message);
|
|
153
|
+
} else if (frame.type === "agent_end" && frame.willContinue !== true) this.#streamedMessages = [];
|
|
154
|
+
return encoded;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claims Bun's singleton stdin reader immediately and exposes a separately readable stream.
|
|
3
|
+
* RPC startup uses this before extension discovery so in-process modules cannot steal protocol input.
|
|
4
|
+
*/
|
|
5
|
+
export function claimRpcInput(): ReadableStream<Uint8Array> {
|
|
6
|
+
const reader = Bun.stdin.stream().getReader();
|
|
7
|
+
let released = false;
|
|
8
|
+
const release = () => {
|
|
9
|
+
if (released) return;
|
|
10
|
+
released = true;
|
|
11
|
+
try {
|
|
12
|
+
reader.releaseLock();
|
|
13
|
+
} catch {}
|
|
14
|
+
};
|
|
15
|
+
return new ReadableStream({
|
|
16
|
+
async pull(controller) {
|
|
17
|
+
try {
|
|
18
|
+
const result = await reader.read();
|
|
19
|
+
if (result.done) {
|
|
20
|
+
release();
|
|
21
|
+
controller.close();
|
|
22
|
+
} else {
|
|
23
|
+
controller.enqueue(result.value);
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
release();
|
|
27
|
+
controller.error(error);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
async cancel() {
|
|
31
|
+
try {
|
|
32
|
+
await reader.cancel();
|
|
33
|
+
} finally {
|
|
34
|
+
release();
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -34,6 +34,8 @@ import type { EventBus } from "../../utils/event-bus";
|
|
|
34
34
|
import { initializeExtensions } from "../runtime-init";
|
|
35
35
|
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
|
36
36
|
import { isRpcHostUriResult, RpcHostUriBridge } from "./host-uris";
|
|
37
|
+
import { RpcFrameEncoder } from "./rpc-frame";
|
|
38
|
+
import { claimRpcInput } from "./rpc-input";
|
|
37
39
|
import { RpcSubagentRegistry, readRpcSubagentTranscript } from "./rpc-subagents";
|
|
38
40
|
import type {
|
|
39
41
|
RpcCommand,
|
|
@@ -607,6 +609,7 @@ export async function runRpcMode(
|
|
|
607
609
|
session: AgentSession,
|
|
608
610
|
setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
|
|
609
611
|
eventBus?: EventBus,
|
|
612
|
+
input: ReadableStream<Uint8Array> = claimRpcInput(),
|
|
610
613
|
): Promise<never> {
|
|
611
614
|
// Signal to RPC clients that the server is ready to accept commands
|
|
612
615
|
// Suppress terminal notifications: they write \x07 (BEL) or OSC sequences directly to
|
|
@@ -615,9 +618,10 @@ export async function runRpcMode(
|
|
|
615
618
|
// may write there.
|
|
616
619
|
process.env.PI_NOTIFICATIONS = "off";
|
|
617
620
|
|
|
618
|
-
|
|
621
|
+
const frameEncoder = new RpcFrameEncoder();
|
|
622
|
+
process.stdout.write(frameEncoder.encode({ type: "ready" }));
|
|
619
623
|
const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
|
|
620
|
-
process.stdout.write(
|
|
624
|
+
process.stdout.write(frameEncoder.encode(obj));
|
|
621
625
|
};
|
|
622
626
|
const emitRpcTitles = shouldEmitRpcTitles();
|
|
623
627
|
|
|
@@ -1327,7 +1331,10 @@ export async function runRpcMode(
|
|
|
1327
1331
|
return (await uiCtx.input(prompt.message, prompt.placeholder, { timeout: 600_000 })) ?? "";
|
|
1328
1332
|
},
|
|
1329
1333
|
});
|
|
1330
|
-
|
|
1334
|
+
// Provider-scoped online refresh so the just-persisted credential
|
|
1335
|
+
// re-runs discovery instead of reusing a fresh authoritative cache
|
|
1336
|
+
// row (#5780).
|
|
1337
|
+
await session.modelRegistry.refreshProvider(command.providerId, "online");
|
|
1331
1338
|
return success(id, "login", { providerId: command.providerId });
|
|
1332
1339
|
} catch (err: unknown) {
|
|
1333
1340
|
return error(id, "login", err instanceof Error ? err.message : String(err));
|
|
@@ -1381,7 +1388,7 @@ export async function runRpcMode(
|
|
|
1381
1388
|
// line is reported as an error frame and the loop keeps running instead of
|
|
1382
1389
|
// throwing out of the generator and killing the whole process (issue #5194).
|
|
1383
1390
|
const decoder = new TextDecoder();
|
|
1384
|
-
for await (const line of readLines(Bun.stdin.stream())) {
|
|
1391
|
+
for await (const line of readLines(input ?? Bun.stdin.stream())) {
|
|
1385
1392
|
const text = decoder.decode(line).trim();
|
|
1386
1393
|
if (!text) continue;
|
|
1387
1394
|
let parsed: unknown;
|
|
@@ -2,6 +2,7 @@ import type { Settings } from "../../config/settings";
|
|
|
2
2
|
import { CURRENT_SETUP_VERSION } from "../setup-version";
|
|
3
3
|
import type { InteractiveModeContext } from "../types";
|
|
4
4
|
import { glyphSetupScene } from "./scenes/glyph";
|
|
5
|
+
import { modelSetupScene } from "./scenes/model";
|
|
5
6
|
import { providersSetupScene } from "./scenes/providers";
|
|
6
7
|
import { themeSetupScene } from "./scenes/theme";
|
|
7
8
|
import type { SetupScene } from "./scenes/types";
|
|
@@ -14,6 +15,7 @@ export { CURRENT_SETUP_VERSION };
|
|
|
14
15
|
|
|
15
16
|
export const ALL_SCENES = [
|
|
16
17
|
providersSetupScene,
|
|
18
|
+
modelSetupScene,
|
|
17
19
|
glyphSetupScene,
|
|
18
20
|
themeSetupScene,
|
|
19
21
|
] as const satisfies readonly SetupScene[];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import type { SgrMouseEvent } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
buildBrowserItems,
|
|
5
|
+
ModelBrowser,
|
|
6
|
+
resolveRoleAssignments,
|
|
7
|
+
sortModelItems,
|
|
8
|
+
} from "../../components/model-browser";
|
|
9
|
+
import { theme } from "../../theme/theme";
|
|
10
|
+
import type { SetupScene, SetupSceneController, SetupSceneHost } from "./types";
|
|
11
|
+
|
|
12
|
+
const MAX_VISIBLE_MODELS = 10;
|
|
13
|
+
const WIZARD_SCREEN_RESERVE = 22;
|
|
14
|
+
|
|
15
|
+
class ModelSceneController implements SetupSceneController {
|
|
16
|
+
title = "Choose your default model";
|
|
17
|
+
subtitle = "Search configured models and save the model used for new sessions.";
|
|
18
|
+
#browser: ModelBrowser;
|
|
19
|
+
#status: string | undefined;
|
|
20
|
+
#selecting = false;
|
|
21
|
+
#disposed = false;
|
|
22
|
+
#browserRowStart = 2;
|
|
23
|
+
|
|
24
|
+
constructor(private readonly host: SetupSceneHost) {
|
|
25
|
+
this.#browser = new ModelBrowser(host.ctx.settings);
|
|
26
|
+
this.#browser.onActivate = item => {
|
|
27
|
+
void this.#select(item.model, item.selector);
|
|
28
|
+
};
|
|
29
|
+
this.#browser.onCancel = () => host.finish("skipped");
|
|
30
|
+
this.#syncModels();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async onMount(): Promise<void> {
|
|
34
|
+
this.#status = theme.fg("muted", "Discovering available models…");
|
|
35
|
+
this.host.requestRender();
|
|
36
|
+
await this.#refreshModels();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
dispose(): void {
|
|
40
|
+
this.#disposed = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
invalidate(): void {
|
|
44
|
+
this.#browser.invalidate();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
handleInput(data: string): void {
|
|
48
|
+
if (this.#selecting) return;
|
|
49
|
+
this.#browser.handleInput(data);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
routeMouse(event: SgrMouseEvent, line: number): void {
|
|
53
|
+
if (this.#selecting) return;
|
|
54
|
+
this.#browser.routeMouse(event, line - this.#browserRowStart);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
render(width: number): readonly string[] {
|
|
58
|
+
const visibleRows = Math.max(
|
|
59
|
+
1,
|
|
60
|
+
Math.min(MAX_VISIBLE_MODELS, this.host.ctx.ui.terminal.rows - WIZARD_SCREEN_RESERVE),
|
|
61
|
+
);
|
|
62
|
+
this.#browser.setMaxVisible(visibleRows);
|
|
63
|
+
const lines = [
|
|
64
|
+
this.#status ?? theme.fg("muted", "Type to search. Enter saves the highlighted model as your default."),
|
|
65
|
+
"",
|
|
66
|
+
];
|
|
67
|
+
this.#browserRowStart = lines.length;
|
|
68
|
+
lines.push(...this.#browser.render(width));
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#syncModels(): void {
|
|
73
|
+
const registry = this.host.ctx.session.modelRegistry;
|
|
74
|
+
const available = registry.getAvailable();
|
|
75
|
+
const roles = resolveRoleAssignments(this.host.ctx.settings, registry.getAll(), available);
|
|
76
|
+
const storage = this.host.ctx.settings.getStorage();
|
|
77
|
+
const items = buildBrowserItems(available);
|
|
78
|
+
sortModelItems(items, { roles, mruOrder: storage?.getModelUsageOrder() ?? [] });
|
|
79
|
+
this.#browser.setRoles(roles);
|
|
80
|
+
this.#browser.setMruOrder(storage?.getModelUsageOrder() ?? []);
|
|
81
|
+
this.#browser.setPerfStats(storage?.getModelPerf() ?? new Map());
|
|
82
|
+
this.#browser.setItems(items);
|
|
83
|
+
|
|
84
|
+
const current = this.host.ctx.session.model;
|
|
85
|
+
if (current) {
|
|
86
|
+
const selector = `${current.provider}/${current.id}`;
|
|
87
|
+
this.#browser.setCurrentSelector(selector);
|
|
88
|
+
this.#browser.selectSelector(selector);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async #refreshModels(): Promise<void> {
|
|
93
|
+
try {
|
|
94
|
+
await this.host.ctx.session.modelRegistry.refresh("online-if-uncached");
|
|
95
|
+
if (this.#disposed) return;
|
|
96
|
+
this.#syncModels();
|
|
97
|
+
this.#status = undefined;
|
|
98
|
+
this.host.requestRender();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (this.#disposed) return;
|
|
101
|
+
this.#status = theme.fg("error", error instanceof Error ? error.message : String(error));
|
|
102
|
+
this.host.requestRender();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async #select(model: Model, selector: string): Promise<void> {
|
|
107
|
+
if (this.#selecting) return;
|
|
108
|
+
this.#selecting = true;
|
|
109
|
+
this.#status = theme.fg("muted", `Saving ${selector} as the default model…`);
|
|
110
|
+
this.host.requestRender();
|
|
111
|
+
try {
|
|
112
|
+
const projectScope = this.host.ctx.settings.get("modelRoleStorage") === "project";
|
|
113
|
+
await this.host.ctx.session.setModel(model, "default", { selector, persist: !projectScope });
|
|
114
|
+
if (projectScope) {
|
|
115
|
+
this.host.ctx.settings.setProjectModelRole("default", selector);
|
|
116
|
+
}
|
|
117
|
+
await this.host.ctx.settings.flush();
|
|
118
|
+
if (!this.#disposed) this.host.finish("done");
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (this.#disposed) return;
|
|
121
|
+
this.#selecting = false;
|
|
122
|
+
this.#status = theme.fg("error", error instanceof Error ? error.message : String(error));
|
|
123
|
+
this.host.requestRender();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Setup step that assigns one available model to the persisted default role. */
|
|
129
|
+
export const modelSetupScene: SetupScene = {
|
|
130
|
+
id: "model",
|
|
131
|
+
title: "Choose your default model",
|
|
132
|
+
minVersion: 1,
|
|
133
|
+
mount: host => new ModelSceneController(host),
|
|
134
|
+
};
|
|
@@ -224,7 +224,9 @@ export class SignInTab implements SetupTab {
|
|
|
224
224
|
onManualCodeInput: () =>
|
|
225
225
|
this.#showPrompt({ message: "Paste the authorization code (or full redirect URL):" }),
|
|
226
226
|
});
|
|
227
|
-
|
|
227
|
+
// Provider-scoped online refresh so the just-persisted credential re-runs
|
|
228
|
+
// discovery instead of reusing a fresh authoritative cache row (#5780).
|
|
229
|
+
await this.host.ctx.session.modelRegistry.refreshProvider(providerId, "online");
|
|
228
230
|
if (this.#disposed) return;
|
|
229
231
|
this.#statusLines = [
|
|
230
232
|
theme.fg("success", `${theme.status.success} Signed in to ${providerId}`),
|
package/src/modes/types.ts
CHANGED
|
@@ -159,6 +159,7 @@ export interface InteractiveModeContext {
|
|
|
159
159
|
goalModeEnabled: boolean;
|
|
160
160
|
goalModePaused: boolean;
|
|
161
161
|
loopModeEnabled: boolean;
|
|
162
|
+
loopModePaused: boolean;
|
|
162
163
|
loopPrompt?: string;
|
|
163
164
|
loopLimit?: LoopLimitRuntime;
|
|
164
165
|
planModePlanFilePath?: string;
|
|
@@ -413,6 +414,7 @@ export interface InteractiveModeContext {
|
|
|
413
414
|
handleGoalModeCommand(rest?: string): Promise<void>;
|
|
414
415
|
handleGuidedGoalCommand(rest?: string): Promise<void>;
|
|
415
416
|
handleLoopCommand(args?: string): Promise<string | undefined>;
|
|
417
|
+
setLoopPrompt(prompt: string): void;
|
|
416
418
|
disableLoopMode(): void;
|
|
417
419
|
pauseLoop(): void;
|
|
418
420
|
handlePlanApproval(details: PlanApprovalDetails): Promise<void>;
|
|
@@ -301,14 +301,18 @@ export class UiHelpers {
|
|
|
301
301
|
let pendingUsage: Usage | undefined;
|
|
302
302
|
let pendingUsageDuration: number | undefined;
|
|
303
303
|
let pendingUsageTtft: number | undefined;
|
|
304
|
+
let pendingUsageTimestamp: number | undefined;
|
|
304
305
|
const flushPendingUsage = () => {
|
|
305
306
|
if (!pendingUsage) return;
|
|
306
307
|
readGroup?.seal();
|
|
307
308
|
readGroup = null;
|
|
308
|
-
this.ctx.chatContainer.addChild(
|
|
309
|
+
this.ctx.chatContainer.addChild(
|
|
310
|
+
createUsageRowBlock(pendingUsage, pendingUsageDuration, pendingUsageTtft, pendingUsageTimestamp),
|
|
311
|
+
);
|
|
309
312
|
pendingUsage = undefined;
|
|
310
313
|
pendingUsageDuration = undefined;
|
|
311
314
|
pendingUsageTtft = undefined;
|
|
315
|
+
pendingUsageTimestamp = undefined;
|
|
312
316
|
};
|
|
313
317
|
// Rebuild-time mirror of the event controller's displaceable-poll
|
|
314
318
|
// bookkeeping: a `hub` wait that found every watched job still running is
|
|
@@ -509,6 +513,7 @@ export class UiHelpers {
|
|
|
509
513
|
: undefined;
|
|
510
514
|
pendingUsageDuration = message.duration;
|
|
511
515
|
pendingUsageTtft = message.ttft;
|
|
516
|
+
pendingUsageTimestamp = message.timestamp;
|
|
512
517
|
} else if (message.role === "toolResult") {
|
|
513
518
|
const pendingReadComponent = this.ctx.pendingTools.get(message.toolCallId);
|
|
514
519
|
const isReadGroupResult =
|
|
@@ -519,10 +524,10 @@ export class UiHelpers {
|
|
|
519
524
|
const images: ImageContent[] = message.content.filter(
|
|
520
525
|
(content): content is ImageContent => content.type === "image",
|
|
521
526
|
);
|
|
522
|
-
if (images.length > 0 && assistantComponent
|
|
527
|
+
if (images.length > 0 && assistantComponent) {
|
|
523
528
|
assistantComponent.setToolResultImages(message.toolCallId, images);
|
|
524
529
|
const hasText = message.content.some(c => c.type === "text");
|
|
525
|
-
if (!hasText) {
|
|
530
|
+
if (!hasText && settings.get("terminal.showImages")) {
|
|
526
531
|
readToolCallArgs.delete(message.toolCallId);
|
|
527
532
|
readToolCallAssistantComponents.delete(message.toolCallId);
|
|
528
533
|
continue;
|
|
@@ -746,6 +751,7 @@ export class UiHelpers {
|
|
|
746
751
|
const hintText = theme.fg("dim", ` ${theme.tree.hook} ${dequeueKey} to edit`);
|
|
747
752
|
this.ctx.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
|
|
748
753
|
}
|
|
754
|
+
this.ctx.ui.requestComponentRender(this.ctx.pendingMessagesContainer);
|
|
749
755
|
}
|
|
750
756
|
|
|
751
757
|
queueCompactionMessage(text: string, mode: "steer" | "followUp", images?: ImageContent[]): void {
|