@oh-my-pi/pi-coding-agent 15.8.2 → 15.9.0
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 +82 -29
- package/dist/types/capability/skill.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +36 -22
- package/dist/types/debug/protocol-probe.d.ts +38 -0
- package/dist/types/debug/terminal-info.d.ts +34 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mcp/transports/stdio.d.ts +16 -7
- package/dist/types/modes/components/assistant-message.d.ts +3 -2
- package/dist/types/modes/components/hook-selector.d.ts +11 -0
- package/dist/types/modes/components/todo-reminder.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +36 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +20 -1
- package/dist/types/plan-mode/plan-handoff.d.ts +20 -0
- package/dist/types/plan-mode/plan-protection.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/indexed-session-storage.d.ts +59 -0
- package/dist/types/session/redis-session-storage.d.ts +12 -85
- package/dist/types/session/session-manager.d.ts +20 -0
- package/dist/types/session/session-storage.d.ts +5 -7
- package/dist/types/session/sql-session-storage.d.ts +16 -85
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/telemetry-export.d.ts +19 -0
- package/dist/types/tiny/compiled-runtime.d.ts +35 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/index.d.ts +4 -2
- package/dist/types/tools/path-utils.d.ts +1 -1
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/{todo-write.d.ts → todo.d.ts} +18 -18
- package/dist/types/utils/jj.d.ts +1 -1
- package/dist/types/utils/session-color.d.ts +7 -2
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +65 -1
- package/package.json +15 -9
- package/scripts/build-binary.ts +12 -0
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +1 -1
- package/src/config/settings-schema.ts +22 -55
- package/src/debug/index.ts +67 -1
- package/src/debug/protocol-probe.ts +267 -0
- package/src/debug/terminal-info.ts +127 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -3
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +3 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/main.ts +12 -0
- package/src/mcp/transports/stdio.ts +37 -12
- package/src/modes/acp/acp-event-mapper.ts +7 -7
- package/src/modes/components/assistant-message.ts +3 -2
- package/src/modes/components/hook-selector.ts +149 -14
- package/src/modes/components/session-selector.ts +36 -5
- package/src/modes/components/status-line/segments.ts +2 -1
- package/src/modes/components/status-line.ts +1 -1
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/todo-reminder.ts +1 -1
- package/src/modes/components/tool-execution.ts +9 -4
- package/src/modes/components/transcript-container.ts +91 -0
- package/src/modes/controllers/event-controller.ts +12 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +18 -6
- package/src/modes/print-mode.ts +5 -0
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/theme/theme.ts +48 -8
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/plan-mode/plan-handoff.ts +37 -0
- package/src/plan-mode/plan-protection.ts +31 -0
- package/src/priority.json +4 -0
- package/src/prompts/goals/goal-continuation.md +1 -1
- package/src/prompts/system/eager-todo.md +3 -3
- package/src/prompts/system/orchestrate-notice.md +5 -5
- package/src/prompts/system/plan-mode-approved.md +14 -17
- package/src/prompts/system/plan-mode-reference.md +3 -6
- package/src/prompts/system/subagent-system-prompt.md +11 -0
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/browser.md +5 -2
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +3 -1
- package/src/prompts/tools/{todo-write.md → todo.md} +8 -0
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +29 -16
- package/src/session/indexed-session-storage.ts +430 -0
- package/src/session/redis-session-storage.ts +66 -377
- package/src/session/session-manager.ts +102 -22
- package/src/session/session-storage.ts +148 -68
- package/src/session/sql-session-storage.ts +131 -382
- package/src/slash-commands/helpers/todo.ts +2 -2
- package/src/task/executor.ts +9 -1
- package/src/task/index.ts +51 -1
- package/src/telemetry-export.ts +126 -0
- package/src/tiny/compiled-runtime.ts +179 -0
- package/src/tiny/worker.ts +24 -2
- package/src/tools/ask.ts +133 -87
- package/src/tools/fetch.ts +17 -4
- package/src/tools/find.ts +2 -2
- package/src/tools/index.ts +6 -4
- package/src/tools/path-utils.ts +16 -7
- package/src/tools/renderers.ts +2 -2
- package/src/tools/search.ts +6 -3
- package/src/tools/{todo-write.ts → todo.ts} +32 -35
- package/src/utils/jj.ts +28 -5
- package/src/utils/session-color.ts +39 -14
- package/src/web/search/index.ts +1 -1
- package/src/web/search/provider.ts +18 -34
- package/src/web/search/types.ts +73 -32
package/src/main.ts
CHANGED
|
@@ -66,6 +66,7 @@ import type { AgentSession } from "./session/agent-session";
|
|
|
66
66
|
import type { AuthStorage } from "./session/auth-storage";
|
|
67
67
|
import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
|
|
68
68
|
import { resolvePromptInput } from "./system-prompt";
|
|
69
|
+
import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
|
|
69
70
|
import { AUTO_THINKING } from "./thinking";
|
|
70
71
|
import type { LspStartupServerInfo } from "./tools";
|
|
71
72
|
import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog";
|
|
@@ -973,6 +974,17 @@ export async function runRootCommand(
|
|
|
973
974
|
sessionOptions.hasUI = isInteractive || mode === "rpc-ui";
|
|
974
975
|
sessionOptions.settings = settingsInstance;
|
|
975
976
|
|
|
977
|
+
// OTEL: register the global OTLP trace exporter when an OTLP endpoint is
|
|
978
|
+
// configured via env, then switch on the agent loop's telemetry so its
|
|
979
|
+
// GenAI spans (invoke_agent / chat / execute_tool) are actually emitted.
|
|
980
|
+
// Both are no-ops when OTEL_EXPORTER_OTLP_ENDPOINT is unset. An empty config
|
|
981
|
+
// is enough to enable telemetry — content capture is governed by the
|
|
982
|
+
// standard OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT env var.
|
|
983
|
+
initTelemetryExport();
|
|
984
|
+
if (isTelemetryExportEnabled()) {
|
|
985
|
+
sessionOptions.telemetry = {};
|
|
986
|
+
}
|
|
987
|
+
|
|
976
988
|
// Handle CLI --api-key as runtime override (not persisted)
|
|
977
989
|
if (parsedArgs.apiKey) {
|
|
978
990
|
if (!sessionOptions.model && !sessionOptions.modelPattern) {
|
|
@@ -25,22 +25,42 @@ interface FrameSink {
|
|
|
25
25
|
flush(): unknown;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Narrow a value to a thenable so a rejection handler can be attached. */
|
|
29
|
+
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
30
|
+
return (
|
|
31
|
+
value != null &&
|
|
32
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
33
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
/**
|
|
29
38
|
* Write a newline-delimited JSON-RPC frame to the subprocess's stdin sink,
|
|
30
|
-
* swallowing synchronous
|
|
39
|
+
* swallowing both synchronous throws and asynchronous rejections so the caller
|
|
40
|
+
* can decide how to react.
|
|
41
|
+
*
|
|
42
|
+
* Bun's `FileSink.write()`/`flush()` can fail two ways once the read end of the
|
|
43
|
+
* pipe has been closed by a subprocess that exited between read-loop ticks:
|
|
44
|
+
* - a synchronous throw (most reliably observed on Windows), and
|
|
45
|
+
* - a *rejected Promise* returned from `write()`/`flush()`, i.e. the EPIPE is
|
|
46
|
+
* surfaced asynchronously (note the `processTicksAndRejections` frame in the
|
|
47
|
+
* stack traces on #1710 and the follow-up report).
|
|
31
48
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* surfaces as an unhandled promise rejection at the call site.
|
|
49
|
+
* A sibling `async` method's `try/catch` only catches the synchronous case; an
|
|
50
|
+
* un-awaited rejected Promise escapes as a fatal unhandled rejection. So we both
|
|
51
|
+
* catch the throw and neutralize any returned promise's rejection.
|
|
36
52
|
*
|
|
37
|
-
* Returns `true` when the frame was accepted
|
|
38
|
-
* sink threw — callers signal transport closure on `false`.
|
|
53
|
+
* Returns `true` when the frame was accepted synchronously, `false` when the
|
|
54
|
+
* sink threw — callers signal transport closure on `false`. An asynchronous
|
|
55
|
+
* failure cannot be reflected in the return value; it is neutralized here and
|
|
56
|
+
* the dead transport is detected by the read loop / request timeout instead.
|
|
39
57
|
*/
|
|
40
58
|
export function writeFrame(stdin: FrameSink, frame: string): boolean {
|
|
41
59
|
try {
|
|
42
|
-
stdin.write(frame);
|
|
43
|
-
stdin.flush();
|
|
60
|
+
const wrote = stdin.write(frame);
|
|
61
|
+
const flushed = stdin.flush();
|
|
62
|
+
if (isThenable(wrote)) wrote.then(undefined, () => {});
|
|
63
|
+
if (isThenable(flushed)) flushed.then(undefined, () => {});
|
|
44
64
|
return true;
|
|
45
65
|
} catch {
|
|
46
66
|
return false;
|
|
@@ -287,11 +307,16 @@ export class StdioTransport implements MCPTransport {
|
|
|
287
307
|
}, timeout);
|
|
288
308
|
}
|
|
289
309
|
|
|
310
|
+
const stdin = this.#process.stdin;
|
|
290
311
|
const message = `${JSON.stringify(request)}\n`;
|
|
291
312
|
try {
|
|
292
|
-
// Bun's FileSink
|
|
293
|
-
|
|
294
|
-
this
|
|
313
|
+
// Await both: Bun's FileSink can surface a broken pipe either as a
|
|
314
|
+
// synchronous throw or as a rejected Promise (the EPIPE arrives on a
|
|
315
|
+
// processTicksAndRejections tick). Awaiting funnels both into this catch
|
|
316
|
+
// so the request rejects cleanly instead of leaving a floating rejected
|
|
317
|
+
// promise that crashes the process via the unhandledRejection handler.
|
|
318
|
+
await stdin.write(message);
|
|
319
|
+
await stdin.flush();
|
|
295
320
|
} catch (error: unknown) {
|
|
296
321
|
cleanup();
|
|
297
322
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
} from "@agentclientprotocol/sdk";
|
|
9
9
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
10
10
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
11
|
-
import type { TodoStatus } from "../../tools/todo
|
|
11
|
+
import type { TodoStatus } from "../../tools/todo";
|
|
12
12
|
|
|
13
13
|
interface MessageProgress {
|
|
14
14
|
textEmitted: boolean;
|
|
@@ -148,7 +148,7 @@ export function mapToolKind(toolName: string): ToolKind {
|
|
|
148
148
|
return "search";
|
|
149
149
|
case "web_search":
|
|
150
150
|
return "fetch";
|
|
151
|
-
case "
|
|
151
|
+
case "todo":
|
|
152
152
|
return "think";
|
|
153
153
|
default:
|
|
154
154
|
return "other";
|
|
@@ -215,7 +215,7 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
215
215
|
update.locations = locations;
|
|
216
216
|
}
|
|
217
217
|
const notifications = [toSessionNotification(sessionId, update)];
|
|
218
|
-
const planUpdate =
|
|
218
|
+
const planUpdate = mapTodoResultToPlanUpdate(event);
|
|
219
219
|
if (planUpdate) {
|
|
220
220
|
notifications.push(toSessionNotification(sessionId, planUpdate));
|
|
221
221
|
}
|
|
@@ -336,13 +336,13 @@ function mapTodoStatus(status: TodoStatus): "pending" | "in_progress" | "complet
|
|
|
336
336
|
return todoStatusMap[status];
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
function
|
|
339
|
+
function mapTodoResultToPlanUpdate(
|
|
340
340
|
event: Extract<AgentSessionEvent, { type: "tool_execution_end" }>,
|
|
341
341
|
): SessionUpdate | undefined {
|
|
342
|
-
if (event.toolName !== "
|
|
342
|
+
if (event.toolName !== "todo" || event.isError) {
|
|
343
343
|
return undefined;
|
|
344
344
|
}
|
|
345
|
-
const phases =
|
|
345
|
+
const phases = extractTodoPhases(event.result);
|
|
346
346
|
if (!Array.isArray(phases)) {
|
|
347
347
|
return undefined;
|
|
348
348
|
}
|
|
@@ -356,7 +356,7 @@ function mapTodoWriteResultToPlanUpdate(
|
|
|
356
356
|
};
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
function
|
|
359
|
+
function extractTodoPhases(result: unknown): unknown {
|
|
360
360
|
if (typeof result !== "object" || result === null || !("details" in result)) {
|
|
361
361
|
return undefined;
|
|
362
362
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AssistantMessage, ImageContent, Usage } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import { Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { Container, Image, type ImageBudget, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
5
|
import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
|
|
@@ -23,6 +23,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
23
23
|
private hideThinkingBlock = false,
|
|
24
24
|
private readonly onImageUpdate?: () => void,
|
|
25
25
|
private readonly thinkingRenderers: readonly AssistantThinkingRenderer[] = [],
|
|
26
|
+
private readonly imageBudget?: ImageBudget,
|
|
26
27
|
) {
|
|
27
28
|
super();
|
|
28
29
|
|
|
@@ -124,7 +125,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
124
125
|
displayImage.data,
|
|
125
126
|
displayImage.mimeType,
|
|
126
127
|
{ fallbackColor: (text: string) => theme.fg("toolOutput", text) },
|
|
127
|
-
resolveImageOptions(),
|
|
128
|
+
{ ...resolveImageOptions(), budget: this.imageBudget, imageKey: key },
|
|
128
129
|
),
|
|
129
130
|
);
|
|
130
131
|
continue;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import {
|
|
6
6
|
Container,
|
|
7
|
+
Ellipsis,
|
|
7
8
|
extractPrintableText,
|
|
8
9
|
fuzzyFilter,
|
|
9
10
|
Markdown,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
Spacer,
|
|
16
17
|
Text,
|
|
17
18
|
type TUI,
|
|
19
|
+
truncateToWidth,
|
|
18
20
|
visibleWidth,
|
|
19
21
|
wrapTextWithAnsi,
|
|
20
22
|
} from "@oh-my-pi/pi-tui";
|
|
@@ -71,6 +73,17 @@ export interface HookSelectorOptions {
|
|
|
71
73
|
/** Indices into the original options that cannot be selected: they render
|
|
72
74
|
* dimmed, are skipped during navigation, and reject enter/timeout. */
|
|
73
75
|
disabledIndices?: readonly number[];
|
|
76
|
+
/** Render a leading radio/checkbox marker before each markable option,
|
|
77
|
+
* matching the ask transcript. "radio" fills the marker on the cursor row
|
|
78
|
+
* (single-choice); "checkbox" reflects {@link checkedIndices} per row
|
|
79
|
+
* (multi-select). Options at or beyond {@link markableCount} keep the plain
|
|
80
|
+
* cursor prefix — used for trailing control rows like "Other"/"Done". */
|
|
81
|
+
selectionMarker?: "radio" | "checkbox";
|
|
82
|
+
/** For `selectionMarker: "checkbox"`: original-indices currently checked. */
|
|
83
|
+
checkedIndices?: readonly number[];
|
|
84
|
+
/** Number of leading options (original order) that receive a selection
|
|
85
|
+
* marker. Defaults to every option when {@link selectionMarker} is set. */
|
|
86
|
+
markableCount?: number;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
export interface HookSelectorOption {
|
|
@@ -140,6 +153,9 @@ export class HookSelectorComponent extends Container {
|
|
|
140
153
|
#searchQuery = "";
|
|
141
154
|
#selectedIndex: number;
|
|
142
155
|
#disabledIndices: Set<number>;
|
|
156
|
+
#selectionMarker: "radio" | "checkbox" | undefined;
|
|
157
|
+
#checkedIndices: Set<number>;
|
|
158
|
+
#markableCount: number;
|
|
143
159
|
#maxVisible: number;
|
|
144
160
|
#listContainer: Container | undefined;
|
|
145
161
|
#outlinedList: OutlinedList | undefined;
|
|
@@ -171,6 +187,13 @@ export class HookSelectorComponent extends Container {
|
|
|
171
187
|
index => Number.isInteger(index) && index >= 0 && index < this.#options.length,
|
|
172
188
|
),
|
|
173
189
|
);
|
|
190
|
+
this.#selectionMarker = opts?.selectionMarker;
|
|
191
|
+
this.#checkedIndices = new Set(
|
|
192
|
+
(opts?.checkedIndices ?? []).filter(
|
|
193
|
+
index => Number.isInteger(index) && index >= 0 && index < this.#options.length,
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
this.#markableCount = Math.max(0, Math.min(opts?.markableCount ?? this.#options.length, this.#options.length));
|
|
174
197
|
this.#selectedIndex = this.#coerceSelectedIndex(opts?.initialIndex ?? 0);
|
|
175
198
|
this.#maxVisible = Math.max(3, opts?.maxVisible ?? 12);
|
|
176
199
|
this.#onSelectCallback = onSelect;
|
|
@@ -278,20 +301,71 @@ export class HookSelectorComponent extends Container {
|
|
|
278
301
|
isSelected: boolean,
|
|
279
302
|
isDisabled: boolean,
|
|
280
303
|
mdTheme: MarkdownTheme,
|
|
304
|
+
descRows: number | "full",
|
|
305
|
+
renderWidth?: number,
|
|
306
|
+
index?: number,
|
|
281
307
|
): string[] {
|
|
282
308
|
const textColor = isDisabled ? "dim" : isSelected ? "accent" : "text";
|
|
283
309
|
const prefixColor = isDisabled ? "dim" : "accent";
|
|
284
310
|
const label = renderInlineMarkdown(option.label, mdTheme, t => theme.fg(textColor, t));
|
|
285
|
-
const
|
|
311
|
+
const marker = index !== undefined ? this.#renderMarkerPrefix(index, isSelected, isDisabled) : undefined;
|
|
312
|
+
const prefix = marker ?? (isSelected ? theme.fg(prefixColor, `${theme.nav.cursor} `) : " ");
|
|
286
313
|
const lines = [prefix + label];
|
|
287
|
-
if (option.description) {
|
|
288
|
-
const descriptionColor = isDisabled ? "dim" : "muted";
|
|
289
|
-
|
|
290
|
-
|
|
314
|
+
if (option.description && descRows !== 0) {
|
|
315
|
+
const descriptionColor: ThemeColor = isDisabled ? "dim" : "muted";
|
|
316
|
+
if (descRows === "full") {
|
|
317
|
+
const description = renderInlineMarkdown(option.description, mdTheme, t => theme.fg(descriptionColor, t));
|
|
318
|
+
lines.push(` ${description}`);
|
|
319
|
+
} else {
|
|
320
|
+
lines.push(
|
|
321
|
+
...this.#wrapDescriptionRows(option.description, descRows, descriptionColor, mdTheme, renderWidth),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
291
324
|
}
|
|
292
325
|
return lines;
|
|
293
326
|
}
|
|
294
327
|
|
|
328
|
+
/** Styled leading marker (`"<glyph> "`) for a markable option row, or
|
|
329
|
+
* `undefined` when no marker applies (control rows beyond `markableCount`,
|
|
330
|
+
* or when {@link selectionMarker} is unset) so the caller falls back to the
|
|
331
|
+
* classic cursor prefix. Radio fills on the cursor row; checkbox reflects
|
|
332
|
+
* the per-row checked state, with the cursor row drawn in accent. */
|
|
333
|
+
#renderMarkerPrefix(index: number, isSelected: boolean, isDisabled: boolean): string | undefined {
|
|
334
|
+
if (this.#selectionMarker === undefined || index >= this.#markableCount) return undefined;
|
|
335
|
+
if (this.#selectionMarker === "radio") {
|
|
336
|
+
const glyph = isSelected ? theme.radio.selected : theme.radio.unselected;
|
|
337
|
+
const color = isDisabled ? "dim" : isSelected ? "accent" : "dim";
|
|
338
|
+
return theme.fg(color, `${glyph} `);
|
|
339
|
+
}
|
|
340
|
+
const checked = this.#checkedIndices.has(index);
|
|
341
|
+
const glyph = checked ? theme.checkbox.checked : theme.checkbox.unchecked;
|
|
342
|
+
const color = isDisabled ? "dim" : isSelected ? "accent" : checked ? "success" : "dim";
|
|
343
|
+
return theme.fg(color, `${glyph} `);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Wrap an option description into indented rows, truncating to `maxRows`
|
|
347
|
+
* with an ellipsis. Pre-wrapping (rather than emitting one long line that the
|
|
348
|
+
* list re-wraps) lets compact mode bound how much of the highlighted option's
|
|
349
|
+
* detail is shown, so every option label stays on screen on short terminals. */
|
|
350
|
+
#wrapDescriptionRows(
|
|
351
|
+
description: string,
|
|
352
|
+
maxRows: number,
|
|
353
|
+
color: ThemeColor,
|
|
354
|
+
mdTheme: MarkdownTheme,
|
|
355
|
+
renderWidth = this.#lastRenderWidth,
|
|
356
|
+
): string[] {
|
|
357
|
+
if (maxRows <= 0) return [];
|
|
358
|
+
const indent = " ";
|
|
359
|
+
const innerWidth = Math.max(1, (renderWidth ?? 80) - 2);
|
|
360
|
+
const bodyWidth = Math.max(1, innerWidth - indent.length);
|
|
361
|
+
const colored = renderInlineMarkdown(description, mdTheme, t => theme.fg(color, t));
|
|
362
|
+
const wrapped = wrapTextWithAnsi(colored, bodyWidth);
|
|
363
|
+
if (wrapped.length <= maxRows) return wrapped.map(row => indent + row);
|
|
364
|
+
const kept = wrapped.slice(0, maxRows);
|
|
365
|
+
kept[maxRows - 1] = truncateToWidth(wrapped.slice(maxRows - 1).join(" "), bodyWidth, Ellipsis.Unicode);
|
|
366
|
+
return kept.map(row => indent + row);
|
|
367
|
+
}
|
|
368
|
+
|
|
295
369
|
#renderedLineRowCount(line: string, renderWidth: number): number {
|
|
296
370
|
const normalized = replaceTabs(line);
|
|
297
371
|
if (this.#outlinedList) {
|
|
@@ -309,10 +383,11 @@ export class HookSelectorComponent extends Container {
|
|
|
309
383
|
renderWidth: number | undefined,
|
|
310
384
|
isSelected: boolean,
|
|
311
385
|
mdTheme: MarkdownTheme,
|
|
386
|
+
descRows: number | "full",
|
|
312
387
|
): number {
|
|
313
|
-
if (renderWidth === undefined) return option.description ? 2 : 1;
|
|
388
|
+
if (renderWidth === undefined) return option.description && descRows !== 0 ? 2 : 1;
|
|
314
389
|
let rows = 0;
|
|
315
|
-
for (const line of this.#renderOptionLines(option, isSelected, false, mdTheme)) {
|
|
390
|
+
for (const line of this.#renderOptionLines(option, isSelected, false, mdTheme, descRows, renderWidth)) {
|
|
316
391
|
rows += this.#renderedLineRowCount(line, renderWidth);
|
|
317
392
|
}
|
|
318
393
|
return rows;
|
|
@@ -322,7 +397,7 @@ export class HookSelectorComponent extends Container {
|
|
|
322
397
|
const themeForRows = mdTheme ?? getMarkdownTheme();
|
|
323
398
|
let rows = 0;
|
|
324
399
|
for (const option of options) {
|
|
325
|
-
rows += this.#optionRowCount(option, renderWidth, false, themeForRows);
|
|
400
|
+
rows += this.#optionRowCount(option, renderWidth, false, themeForRows, "full");
|
|
326
401
|
}
|
|
327
402
|
return rows;
|
|
328
403
|
}
|
|
@@ -331,19 +406,37 @@ export class HookSelectorComponent extends Container {
|
|
|
331
406
|
total: number,
|
|
332
407
|
renderWidth?: number,
|
|
333
408
|
mdTheme: MarkdownTheme = getMarkdownTheme(),
|
|
409
|
+
compact = false,
|
|
334
410
|
): { startIndex: number; endIndex: number } {
|
|
335
411
|
if (total === 0) return { startIndex: 0, endIndex: 0 };
|
|
336
412
|
|
|
413
|
+
// In compact mode every option contributes only its label rows; the
|
|
414
|
+
// highlighted option's description is layered on afterwards (see
|
|
415
|
+
// #updateList), so the window is sized to keep as many labels visible as
|
|
416
|
+
// possible rather than letting one long description swallow the budget.
|
|
417
|
+
const descMode: number | "full" = compact ? 0 : "full";
|
|
337
418
|
const rowBudget = Math.max(1, this.#maxVisible);
|
|
338
419
|
const selectedIndex = Math.max(0, Math.min(this.#selectedIndex, total - 1));
|
|
339
420
|
let startIndex = selectedIndex;
|
|
340
421
|
let endIndex = selectedIndex + 1;
|
|
341
|
-
let rows = this.#optionRowCount(
|
|
422
|
+
let rows = this.#optionRowCount(
|
|
423
|
+
this.#filteredOptions[selectedIndex]!.option,
|
|
424
|
+
renderWidth,
|
|
425
|
+
true,
|
|
426
|
+
mdTheme,
|
|
427
|
+
descMode,
|
|
428
|
+
);
|
|
342
429
|
let beforeRows = 0;
|
|
343
430
|
const targetBeforeRows = Math.max(0, Math.floor((rowBudget - rows) / 2));
|
|
344
431
|
|
|
345
432
|
while (startIndex > 0) {
|
|
346
|
-
const cost = this.#optionRowCount(
|
|
433
|
+
const cost = this.#optionRowCount(
|
|
434
|
+
this.#filteredOptions[startIndex - 1]!.option,
|
|
435
|
+
renderWidth,
|
|
436
|
+
false,
|
|
437
|
+
mdTheme,
|
|
438
|
+
descMode,
|
|
439
|
+
);
|
|
347
440
|
if (beforeRows + cost > targetBeforeRows || rows + cost > rowBudget) break;
|
|
348
441
|
startIndex--;
|
|
349
442
|
beforeRows += cost;
|
|
@@ -351,14 +444,26 @@ export class HookSelectorComponent extends Container {
|
|
|
351
444
|
}
|
|
352
445
|
|
|
353
446
|
while (endIndex < total) {
|
|
354
|
-
const cost = this.#optionRowCount(
|
|
447
|
+
const cost = this.#optionRowCount(
|
|
448
|
+
this.#filteredOptions[endIndex]!.option,
|
|
449
|
+
renderWidth,
|
|
450
|
+
false,
|
|
451
|
+
mdTheme,
|
|
452
|
+
descMode,
|
|
453
|
+
);
|
|
355
454
|
if (rows + cost > rowBudget) break;
|
|
356
455
|
endIndex++;
|
|
357
456
|
rows += cost;
|
|
358
457
|
}
|
|
359
458
|
|
|
360
459
|
while (startIndex > 0) {
|
|
361
|
-
const cost = this.#optionRowCount(
|
|
460
|
+
const cost = this.#optionRowCount(
|
|
461
|
+
this.#filteredOptions[startIndex - 1]!.option,
|
|
462
|
+
renderWidth,
|
|
463
|
+
false,
|
|
464
|
+
mdTheme,
|
|
465
|
+
descMode,
|
|
466
|
+
);
|
|
362
467
|
if (rows + cost > rowBudget) break;
|
|
363
468
|
startIndex--;
|
|
364
469
|
rows += cost;
|
|
@@ -371,13 +476,43 @@ export class HookSelectorComponent extends Container {
|
|
|
371
476
|
const lines: string[] = [];
|
|
372
477
|
const total = this.#filteredOptions.length;
|
|
373
478
|
const mdTheme = getMarkdownTheme();
|
|
374
|
-
|
|
479
|
+
// Compact mode kicks in exactly when the fully-expanded list (all
|
|
480
|
+
// descriptions) would overflow the row budget — the same condition that
|
|
481
|
+
// enables search. There we collapse every option to its label and show
|
|
482
|
+
// only the highlighted option's description, so the whole menu stays
|
|
483
|
+
// visible on short terminals instead of collapsing to a single entry.
|
|
484
|
+
const compact = this.#isSearchEnabled(renderWidth, mdTheme);
|
|
485
|
+
const { startIndex, endIndex } = this.#getVisibleOptionRange(total, renderWidth, mdTheme, compact);
|
|
486
|
+
|
|
487
|
+
let selectedDescRows = 0;
|
|
488
|
+
if (compact && renderWidth !== undefined) {
|
|
489
|
+
let labelRows = 0;
|
|
490
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
491
|
+
const filtered = this.#filteredOptions[i];
|
|
492
|
+
if (filtered === undefined) continue;
|
|
493
|
+
labelRows += this.#optionRowCount(filtered.option, renderWidth, i === this.#selectedIndex, mdTheme, 0);
|
|
494
|
+
}
|
|
495
|
+
// Reserve one row for the status line; give the remainder to the
|
|
496
|
+
// highlighted option's description.
|
|
497
|
+
selectedDescRows = Math.max(0, Math.max(1, this.#maxVisible) - labelRows - 1);
|
|
498
|
+
}
|
|
375
499
|
|
|
376
500
|
for (let i = startIndex; i < endIndex; i++) {
|
|
377
501
|
const filtered = this.#filteredOptions[i];
|
|
378
502
|
if (filtered === undefined) continue;
|
|
379
503
|
const isSelected = i === this.#selectedIndex;
|
|
380
|
-
|
|
504
|
+
const descMode: number | "full" = compact ? (isSelected ? selectedDescRows : 0) : "full";
|
|
505
|
+
lines.push(
|
|
506
|
+
...this.#renderOptionLines(
|
|
507
|
+
filtered.option,
|
|
508
|
+
isSelected,
|
|
509
|
+
this.#isDisabled(filtered.index),
|
|
510
|
+
mdTheme,
|
|
511
|
+
descMode,
|
|
512
|
+
renderWidth,
|
|
513
|
+
filtered.index,
|
|
514
|
+
),
|
|
515
|
+
);
|
|
381
516
|
}
|
|
382
517
|
|
|
383
518
|
if (total === 0) {
|
|
@@ -14,11 +14,34 @@ import {
|
|
|
14
14
|
import { formatBytes } from "@oh-my-pi/pi-utils";
|
|
15
15
|
import { theme } from "../../modes/theme/theme";
|
|
16
16
|
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
17
|
-
import type { SessionInfo } from "../../session/session-manager";
|
|
17
|
+
import type { SessionInfo, SessionStatus } from "../../session/session-manager";
|
|
18
18
|
import { shortenPath } from "../../tools/render-utils";
|
|
19
19
|
import { DynamicBorder } from "./dynamic-border";
|
|
20
20
|
import { HookSelectorComponent } from "./hook-selector";
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Themed glyph + colored label for a session's lifecycle status, or `undefined`
|
|
24
|
+
* when there is nothing useful to show (`unknown`/unset) so the metadata line
|
|
25
|
+
* stays uncluttered. The glyph resolves through the active symbol preset
|
|
26
|
+
* (nerdfont / unicode / ascii) via `theme.status.*`.
|
|
27
|
+
*/
|
|
28
|
+
function formatSessionStatus(status: SessionStatus | undefined): string | undefined {
|
|
29
|
+
switch (status) {
|
|
30
|
+
case "complete":
|
|
31
|
+
return theme.fg("success", `${theme.status.success} done`);
|
|
32
|
+
case "interrupted":
|
|
33
|
+
return theme.fg("warning", `${theme.status.warning} interrupted`);
|
|
34
|
+
case "aborted":
|
|
35
|
+
return theme.fg("muted", `${theme.status.aborted} aborted`);
|
|
36
|
+
case "error":
|
|
37
|
+
return theme.fg("error", `${theme.status.error} error`);
|
|
38
|
+
case "pending":
|
|
39
|
+
return theme.fg("accent", `${theme.status.pending} pending`);
|
|
40
|
+
default:
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
/** Returns the IDs of sessions whose recorded prompts match a query, best first. */
|
|
23
46
|
export type SessionHistoryMatcher = (query: string) => string[];
|
|
24
47
|
|
|
@@ -227,13 +250,21 @@ class SessionList implements Component {
|
|
|
227
250
|
lines.push(messageLine);
|
|
228
251
|
}
|
|
229
252
|
|
|
230
|
-
// Metadata line: date + file size (+ project dir in
|
|
253
|
+
// Metadata line: date + file size + lifecycle status (+ project dir in
|
|
254
|
+
// all-projects scope). The status segment carries its own color, so each
|
|
255
|
+
// segment is dimmed individually rather than wrapping the whole line.
|
|
256
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
257
|
+
const dot = dim(theme.sep.dot);
|
|
231
258
|
const modified = formatDate(session.modified);
|
|
232
|
-
let metadata = ` ${modified} ${
|
|
259
|
+
let metadata = ` ${dim(modified)} ${dot} ${dim(formatBytes(session.size))}`;
|
|
260
|
+
const status = formatSessionStatus(session.status);
|
|
261
|
+
if (status) {
|
|
262
|
+
metadata += ` ${dot} ${status}`;
|
|
263
|
+
}
|
|
233
264
|
if (this.#showCwd && session.cwd) {
|
|
234
|
-
metadata += ` ${
|
|
265
|
+
metadata += ` ${dot} ${dim(shortenPath(session.cwd))}`;
|
|
235
266
|
}
|
|
236
|
-
const metadataLine =
|
|
267
|
+
const metadataLine = truncateToWidth(metadata, width);
|
|
237
268
|
|
|
238
269
|
lines.push(metadataLine);
|
|
239
270
|
lines.push(""); // Blank line between sessions
|
|
@@ -485,7 +485,8 @@ const sessionNameSegment: StatusLineSegment = {
|
|
|
485
485
|
const name = sessionManager?.getSessionName();
|
|
486
486
|
if (!name) return { content: "", visible: false };
|
|
487
487
|
|
|
488
|
-
const ansi =
|
|
488
|
+
const ansi =
|
|
489
|
+
getSessionAccentAnsi(getSessionAccentHex(name, theme.accentSurfaceLuminance)) ?? theme.getFgAnsi("accent");
|
|
489
490
|
return { content: `${ansi}${sanitizeStatusText(name)}\x1b[39m`, visible: true };
|
|
490
491
|
},
|
|
491
492
|
};
|
|
@@ -764,7 +764,7 @@ export class StatusLineComponent implements Component {
|
|
|
764
764
|
const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
|
|
765
765
|
const sessionName =
|
|
766
766
|
effectiveSettings.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
|
|
767
|
-
const accentHex = sessionName ? getSessionAccentHex(sessionName) : undefined;
|
|
767
|
+
const accentHex = sessionName ? getSessionAccentHex(sessionName, theme.accentSurfaceLuminance) : undefined;
|
|
768
768
|
const gapColor = getSessionAccentAnsi(accentHex) ?? theme.getFgAnsi("border");
|
|
769
769
|
const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[39m`;
|
|
770
770
|
return leftGroup + gapFill + rightGroup;
|
|
@@ -5,7 +5,7 @@ Find out which model you emotionally abuse the most with `omp stats`
|
|
|
5
5
|
Try task isolation to create CoW worktrees
|
|
6
6
|
Your LLM can call an LLM using `llm(x...)`. Have a big batch of tasks? Ask clanker to use it!
|
|
7
7
|
Spaghetti code? Try complaining with /omfg
|
|
8
|
-
Did you know? Each kitty/tmux split keeps its own session — `omp -c` resumes the right one
|
|
8
|
+
Did you know? Each kitty/tmux/cmux split keeps its own session — `omp -c` resumes the right one
|
|
9
9
|
Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
|
|
10
10
|
Say `orchestrate` in your message to drive a multi-phase task with parallel subagents — watch it glow as you type
|
|
11
11
|
Say `workflow` in your message to drive the task with parallel subagents in eval — watch it glow as you type
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box, Container, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { theme } from "../../modes/theme/theme";
|
|
3
|
-
import type { TodoItem } from "../../tools/todo
|
|
3
|
+
import type { TodoItem } from "../../tools/todo";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Component that renders a todo completion reminder notification.
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
} from "../../tools/json-tree";
|
|
33
33
|
import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
|
|
34
34
|
import { toolRenderers } from "../../tools/renderers";
|
|
35
|
-
import {
|
|
35
|
+
import { TODO_STRIKE_TOTAL_FRAMES } from "../../tools/todo";
|
|
36
36
|
import { renderStatusLine } from "../../tui";
|
|
37
37
|
import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
38
38
|
import { renderDiff } from "./diff";
|
|
@@ -149,6 +149,10 @@ const SPINNER_RENDER_INTERVAL_MS = 16;
|
|
|
149
149
|
* 60fps render cadence (mirrors `Loader`). */
|
|
150
150
|
const SPINNER_GLYPH_ADVANCE_MS = 80;
|
|
151
151
|
|
|
152
|
+
// Stable per-instance counter so each tool execution's inline images get a
|
|
153
|
+
// graphics id that survives child re-creation (the image budget keys off it).
|
|
154
|
+
let toolExecutionInstanceSeq = 0;
|
|
155
|
+
|
|
152
156
|
/**
|
|
153
157
|
* Component that renders a tool call with its result (updateable)
|
|
154
158
|
*/
|
|
@@ -158,6 +162,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
158
162
|
#multiFileBoxes: (Box | Spacer)[] = []; // Extra boxes for multi-file edit results
|
|
159
163
|
#imageComponents: Image[] = [];
|
|
160
164
|
#imageSpacers: Spacer[] = [];
|
|
165
|
+
readonly #instanceId = ++toolExecutionInstanceSeq;
|
|
161
166
|
#toolName: string;
|
|
162
167
|
#toolLabel: string;
|
|
163
168
|
#args: any;
|
|
@@ -447,7 +452,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
447
452
|
}
|
|
448
453
|
|
|
449
454
|
#updateTodoStrikeAnimation(): void {
|
|
450
|
-
if (this.#toolName !== "
|
|
455
|
+
if (this.#toolName !== "todo" || this.#isPartial || this.#result?.isError) {
|
|
451
456
|
this.#stopTodoStrikeAnimation();
|
|
452
457
|
return;
|
|
453
458
|
}
|
|
@@ -462,7 +467,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
462
467
|
this.#renderState.spinnerFrame = 0;
|
|
463
468
|
this.#todoStrikeInterval = setInterval(() => {
|
|
464
469
|
const nextFrame = (this.#spinnerFrame ?? 0) + 1;
|
|
465
|
-
if (nextFrame >
|
|
470
|
+
if (nextFrame > TODO_STRIKE_TOTAL_FRAMES) {
|
|
466
471
|
this.#stopTodoStrikeAnimation();
|
|
467
472
|
} else {
|
|
468
473
|
this.#spinnerFrame = nextFrame;
|
|
@@ -754,7 +759,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
754
759
|
imageData,
|
|
755
760
|
imageMimeType,
|
|
756
761
|
{ fallbackColor: (s: string) => theme.fg("toolOutput", s) },
|
|
757
|
-
resolveImageOptions(),
|
|
762
|
+
{ ...resolveImageOptions(), budget: this.#ui.imageBudget, imageKey: `te${this.#instanceId}:${i}` },
|
|
758
763
|
);
|
|
759
764
|
this.#imageComponents.push(imageComponent);
|
|
760
765
|
this.addChild(imageComponent);
|