@oh-my-pi/pi-coding-agent 15.8.3 → 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 +74 -31
- 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/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/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/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} +5 -5
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +14 -14
- 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/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
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
TINY_TITLE_MODEL_VALUES,
|
|
24
24
|
} from "../tiny/models";
|
|
25
25
|
import { EDIT_MODES } from "../utils/edit-mode";
|
|
26
|
+
import { SEARCH_PROVIDER_OPTIONS, SEARCH_PROVIDER_PREFERENCES } from "../web/search/types";
|
|
26
27
|
|
|
27
28
|
/** Unified settings schema - single source of truth for all settings.
|
|
28
29
|
* Unified settings schema - single source of truth for all settings.
|
|
@@ -608,6 +609,24 @@ export const SETTINGS_SCHEMA = {
|
|
|
608
609
|
"Maximum height in terminal rows for inline images (default 20). Set to 0 to use only the viewport-based limit (60% of terminal height).",
|
|
609
610
|
},
|
|
610
611
|
|
|
612
|
+
"tui.maxInlineImages": {
|
|
613
|
+
type: "number",
|
|
614
|
+
default: 8,
|
|
615
|
+
description:
|
|
616
|
+
"Maximum number of inline images kept as live terminal graphics (default 8). Older images fall back to a text placeholder via a full redraw once the limit is exceeded. Set to 0 to keep every image (no limit).",
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
"tui.textSizing": {
|
|
620
|
+
type: "boolean",
|
|
621
|
+
default: false,
|
|
622
|
+
ui: {
|
|
623
|
+
tab: "appearance",
|
|
624
|
+
label: "Large Headings (Kitty)",
|
|
625
|
+
description:
|
|
626
|
+
"Render Markdown H1 headings at 2x scale using Kitty's OSC 66 text-sizing protocol. Only takes effect on Kitty terminals; ignored everywhere else. Off by default.",
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
|
|
611
630
|
"tui.hyperlinks": {
|
|
612
631
|
type: "enum",
|
|
613
632
|
values: ["off", "auto", "always"] as const,
|
|
@@ -2104,7 +2123,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2104
2123
|
"todo.enabled": {
|
|
2105
2124
|
type: "boolean",
|
|
2106
2125
|
default: true,
|
|
2107
|
-
ui: { tab: "tools", label: "Todos", description: "Enable the
|
|
2126
|
+
ui: { tab: "tools", label: "Todos", description: "Enable the todo tool for task tracking" },
|
|
2108
2127
|
},
|
|
2109
2128
|
|
|
2110
2129
|
"todo.reminders": {
|
|
@@ -2866,65 +2885,13 @@ export const SETTINGS_SCHEMA = {
|
|
|
2866
2885
|
// Provider selection
|
|
2867
2886
|
"providers.webSearch": {
|
|
2868
2887
|
type: "enum",
|
|
2869
|
-
values:
|
|
2870
|
-
"auto",
|
|
2871
|
-
"exa",
|
|
2872
|
-
"brave",
|
|
2873
|
-
"jina",
|
|
2874
|
-
"kimi",
|
|
2875
|
-
"zai",
|
|
2876
|
-
"perplexity",
|
|
2877
|
-
"anthropic",
|
|
2878
|
-
"gemini",
|
|
2879
|
-
"codex",
|
|
2880
|
-
"tavily",
|
|
2881
|
-
"kagi",
|
|
2882
|
-
"synthetic",
|
|
2883
|
-
"parallel",
|
|
2884
|
-
"searxng",
|
|
2885
|
-
] as const,
|
|
2888
|
+
values: SEARCH_PROVIDER_PREFERENCES,
|
|
2886
2889
|
default: "auto",
|
|
2887
2890
|
ui: {
|
|
2888
2891
|
tab: "providers",
|
|
2889
2892
|
label: "Web Search Provider",
|
|
2890
2893
|
description: "Provider for web search tool",
|
|
2891
|
-
options:
|
|
2892
|
-
{
|
|
2893
|
-
value: "auto",
|
|
2894
|
-
label: "Auto",
|
|
2895
|
-
description: "Preferred web-search provider",
|
|
2896
|
-
},
|
|
2897
|
-
{ value: "exa", label: "Exa", description: "Requires EXA_API_KEY" },
|
|
2898
|
-
{ value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
|
|
2899
|
-
{ value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
|
|
2900
|
-
{ value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
|
|
2901
|
-
{
|
|
2902
|
-
value: "perplexity",
|
|
2903
|
-
label: "Perplexity",
|
|
2904
|
-
description: "Requires PERPLEXITY_COOKIES or PERPLEXITY_API_KEY",
|
|
2905
|
-
},
|
|
2906
|
-
{
|
|
2907
|
-
value: "anthropic",
|
|
2908
|
-
label: "Anthropic",
|
|
2909
|
-
description: "Claude's native web_search tool (uses Anthropic OAuth or ANTHROPIC_API_KEY)",
|
|
2910
|
-
},
|
|
2911
|
-
{
|
|
2912
|
-
value: "codex",
|
|
2913
|
-
label: "OpenAI",
|
|
2914
|
-
description: "OpenAI's native web_search (uses ChatGPT OAuth via /login openai-codex)",
|
|
2915
|
-
},
|
|
2916
|
-
{
|
|
2917
|
-
value: "gemini",
|
|
2918
|
-
label: "Gemini",
|
|
2919
|
-
description: "Google Search grounding via Gemini (uses google-gemini-cli or google-antigravity OAuth)",
|
|
2920
|
-
},
|
|
2921
|
-
{ value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
|
|
2922
|
-
{ value: "tavily", label: "Tavily", description: "Requires TAVILY_API_KEY" },
|
|
2923
|
-
{ value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY (Kagi V1 Search API)" },
|
|
2924
|
-
{ value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
|
|
2925
|
-
{ value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
|
|
2926
|
-
{ value: "searxng", label: "SearXNG", description: "Requires SEARXNG_ENDPOINT or searxng.endpoint" },
|
|
2927
|
-
],
|
|
2894
|
+
options: SEARCH_PROVIDER_OPTIONS,
|
|
2928
2895
|
},
|
|
2929
2896
|
},
|
|
2930
2897
|
"providers.image": {
|
package/src/debug/index.ts
CHANGED
|
@@ -6,7 +6,17 @@
|
|
|
6
6
|
import * as fs from "node:fs/promises";
|
|
7
7
|
import * as url from "node:url";
|
|
8
8
|
import { getWorkProfile } from "@oh-my-pi/pi-natives";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
Container,
|
|
11
|
+
isNotificationSuppressed,
|
|
12
|
+
Loader,
|
|
13
|
+
type SelectItem,
|
|
14
|
+
SelectList,
|
|
15
|
+
Spacer,
|
|
16
|
+
TERMINAL,
|
|
17
|
+
type TerminalNotification,
|
|
18
|
+
Text,
|
|
19
|
+
} from "@oh-my-pi/pi-tui";
|
|
10
20
|
import { getSessionsDir } from "@oh-my-pi/pi-utils";
|
|
11
21
|
import { DynamicBorder } from "../modes/components/dynamic-border";
|
|
12
22
|
import { getSelectListTheme, getSymbolTheme, theme } from "../modes/theme/theme";
|
|
@@ -15,10 +25,12 @@ import { formatBytes } from "../tools/render-utils";
|
|
|
15
25
|
import { openPath } from "../utils/open";
|
|
16
26
|
import { DebugLogViewerComponent } from "./log-viewer";
|
|
17
27
|
import { generateHeapSnapshotData, type ProfilerSession, startCpuProfile } from "./profiler";
|
|
28
|
+
import { buildSampleImage, ProtocolProbeComponent } from "./protocol-probe";
|
|
18
29
|
import { RawSseViewerComponent } from "./raw-sse";
|
|
19
30
|
import { resolveRawSseDebugBuffer } from "./raw-sse-buffer";
|
|
20
31
|
import { clearArtifactCache, createDebugLogSource, createReportBundle, getArtifactCacheStats } from "./report-bundle";
|
|
21
32
|
import { collectSystemInfo, formatSystemInfo } from "./system-info";
|
|
33
|
+
import { collectTerminalState, formatTerminalState } from "./terminal-info";
|
|
22
34
|
|
|
23
35
|
/** Debug menu options */
|
|
24
36
|
const DEBUG_MENU_ITEMS: SelectItem[] = [
|
|
@@ -29,6 +41,12 @@ const DEBUG_MENU_ITEMS: SelectItem[] = [
|
|
|
29
41
|
{ value: "memory", label: "Report: memory issue", description: "Heap snapshot + bundle" },
|
|
30
42
|
{ value: "logs", label: "View: recent logs", description: "Show last 50 log entries" },
|
|
31
43
|
{ value: "system", label: "View: system info", description: "Show environment details" },
|
|
44
|
+
{ value: "terminal", label: "View: terminal state", description: "Subprotocols, geometry, scrollback strategy" },
|
|
45
|
+
{
|
|
46
|
+
value: "protocols",
|
|
47
|
+
label: "Test: terminal protocols",
|
|
48
|
+
description: "Styling, links, text sizing, graphics, notify",
|
|
49
|
+
},
|
|
32
50
|
{ value: "raw-sse", label: "View: raw SSE stream", description: "Show live provider SSE frames" },
|
|
33
51
|
{
|
|
34
52
|
value: "transcript",
|
|
@@ -106,6 +124,12 @@ export class DebugSelectorComponent extends Container {
|
|
|
106
124
|
case "system":
|
|
107
125
|
await this.#handleViewSystemInfo();
|
|
108
126
|
break;
|
|
127
|
+
case "terminal":
|
|
128
|
+
await this.#handleViewTerminalState();
|
|
129
|
+
break;
|
|
130
|
+
case "protocols":
|
|
131
|
+
await this.#handleViewProtocols();
|
|
132
|
+
break;
|
|
109
133
|
case "transcript":
|
|
110
134
|
await this.#handleTranscriptExport();
|
|
111
135
|
break;
|
|
@@ -352,6 +376,48 @@ export class DebugSelectorComponent extends Container {
|
|
|
352
376
|
this.ctx.ui.requestRender();
|
|
353
377
|
}
|
|
354
378
|
|
|
379
|
+
async #handleViewTerminalState(): Promise<void> {
|
|
380
|
+
const info = collectTerminalState({
|
|
381
|
+
columns: this.ctx.ui.terminal.columns,
|
|
382
|
+
rows: this.ctx.ui.terminal.rows,
|
|
383
|
+
synchronizedOutput: this.ctx.ui.synchronizedOutput,
|
|
384
|
+
});
|
|
385
|
+
const formatted = formatTerminalState(info);
|
|
386
|
+
|
|
387
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
388
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
389
|
+
this.ctx.chatContainer.addChild(new Text(formatted, 1, 0));
|
|
390
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
391
|
+
this.ctx.ui.requestRender();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async #handleViewProtocols(): Promise<void> {
|
|
395
|
+
// Fire the desktop notification as a real side effect, then render a
|
|
396
|
+
// panel that samples every other special protocol and reports the
|
|
397
|
+
// notification outcome.
|
|
398
|
+
const suppressed = isNotificationSuppressed();
|
|
399
|
+
if (!suppressed) {
|
|
400
|
+
const sessionName = this.ctx.sessionManager.getSessionName();
|
|
401
|
+
const notification: TerminalNotification = {
|
|
402
|
+
title: sessionName || "Oh My Pi",
|
|
403
|
+
body: "Terminal protocol test",
|
|
404
|
+
type: "test",
|
|
405
|
+
actions: "focus",
|
|
406
|
+
};
|
|
407
|
+
TERMINAL.sendNotification(notification);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
411
|
+
this.ctx.chatContainer.addChild(
|
|
412
|
+
new ProtocolProbeComponent({
|
|
413
|
+
image: buildSampleImage(),
|
|
414
|
+
imageBudget: this.ctx.ui.imageBudget,
|
|
415
|
+
notificationSuppressed: suppressed,
|
|
416
|
+
}),
|
|
417
|
+
);
|
|
418
|
+
this.ctx.ui.requestRender();
|
|
419
|
+
}
|
|
420
|
+
|
|
355
421
|
async #handleTranscriptExport(): Promise<void> {
|
|
356
422
|
await this.ctx.handleDebugTranscriptCommand();
|
|
357
423
|
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal protocol smoke-test panel for the debug menu.
|
|
3
|
+
*
|
|
4
|
+
* Exercises every "special" escape protocol the renderer can emit so a human
|
|
5
|
+
* can eyeball which ones the active terminal actually honors:
|
|
6
|
+
* - SGR text styling + 24-bit truecolor,
|
|
7
|
+
* - OSC 8 hyperlinks,
|
|
8
|
+
* - OSC 66 text sizing (large text),
|
|
9
|
+
* - inline graphics (Kitty / iTerm2 / Sixel),
|
|
10
|
+
* - OSC 9 / OSC 99 desktop notifications (fired by the caller).
|
|
11
|
+
*
|
|
12
|
+
* The sample image is generated in-process (a deterministic RGB gradient PNG)
|
|
13
|
+
* so the graphics test needs no asset on disk and works across all three image
|
|
14
|
+
* protocols, each of which decodes a standard PNG.
|
|
15
|
+
*/
|
|
16
|
+
import * as zlib from "node:zlib";
|
|
17
|
+
import {
|
|
18
|
+
type Component,
|
|
19
|
+
Container,
|
|
20
|
+
encodeTextSized,
|
|
21
|
+
Image,
|
|
22
|
+
type ImageBudget,
|
|
23
|
+
ImageProtocol,
|
|
24
|
+
NotifyProtocol,
|
|
25
|
+
Spacer,
|
|
26
|
+
TERMINAL,
|
|
27
|
+
Text,
|
|
28
|
+
type TextSizingScale,
|
|
29
|
+
} from "@oh-my-pi/pi-tui";
|
|
30
|
+
import { DynamicBorder } from "../modes/components/dynamic-border";
|
|
31
|
+
import { theme } from "../modes/theme/theme";
|
|
32
|
+
|
|
33
|
+
const PNG_SIGNATURE = Uint8Array.of(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a);
|
|
34
|
+
|
|
35
|
+
/** Frame a PNG chunk: 4-byte big-endian length, type+data, then the CRC-32 of type+data. */
|
|
36
|
+
function pngChunk(type: string, data: Uint8Array): Uint8Array {
|
|
37
|
+
const body = Buffer.concat([Buffer.from(type, "latin1"), data]);
|
|
38
|
+
const out = Buffer.alloc(body.length + 8);
|
|
39
|
+
out.writeUInt32BE(data.length, 0);
|
|
40
|
+
body.copy(out, 4);
|
|
41
|
+
out.writeUInt32BE(Bun.hash.crc32(body) >>> 0, out.length - 4);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Encode raw 8-bit RGB pixels (`width * height * 3` bytes, row-major) as a PNG
|
|
47
|
+
* (color type 2, no interlacing). The IDAT payload is a real zlib stream from
|
|
48
|
+
* {@link zlib.deflateSync}, so the output is a fully valid PNG that every image
|
|
49
|
+
* protocol — including Sixel, which decodes the bytes natively — accepts.
|
|
50
|
+
*/
|
|
51
|
+
export function encodeRgbPng(width: number, height: number, rgb: Uint8Array): Uint8Array {
|
|
52
|
+
const ihdr = Buffer.alloc(13);
|
|
53
|
+
ihdr.writeUInt32BE(width, 0);
|
|
54
|
+
ihdr.writeUInt32BE(height, 4);
|
|
55
|
+
ihdr[8] = 8; // bit depth
|
|
56
|
+
ihdr[9] = 2; // color type: truecolor RGB
|
|
57
|
+
// compression (0), filter (0), interlace (0) are already zeroed.
|
|
58
|
+
|
|
59
|
+
const stride = width * 3;
|
|
60
|
+
// Each scanline is prefixed with a filter-type byte (0 = None).
|
|
61
|
+
const raw = Buffer.alloc((stride + 1) * height);
|
|
62
|
+
for (let y = 0; y < height; y++) {
|
|
63
|
+
raw.set(rgb.subarray(y * stride, y * stride + stride), y * (stride + 1) + 1);
|
|
64
|
+
}
|
|
65
|
+
const idat = zlib.deflateSync(raw);
|
|
66
|
+
|
|
67
|
+
return Buffer.concat([
|
|
68
|
+
PNG_SIGNATURE,
|
|
69
|
+
pngChunk("IHDR", ihdr),
|
|
70
|
+
pngChunk("IDAT", idat),
|
|
71
|
+
pngChunk("IEND", new Uint8Array(0)),
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface SampleImage {
|
|
76
|
+
base64: string;
|
|
77
|
+
mimeType: string;
|
|
78
|
+
dimensions: { widthPx: number; heightPx: number };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Build a deterministic RGB gradient PNG (red across, green down, constant blue). */
|
|
82
|
+
export function buildSampleImage(width = 192, height = 128): SampleImage {
|
|
83
|
+
const denomX = Math.max(1, width - 1);
|
|
84
|
+
const denomY = Math.max(1, height - 1);
|
|
85
|
+
const rgb = new Uint8Array(width * height * 3);
|
|
86
|
+
for (let y = 0; y < height; y++) {
|
|
87
|
+
for (let x = 0; x < width; x++) {
|
|
88
|
+
const i = (y * width + x) * 3;
|
|
89
|
+
rgb[i] = Math.round((x / denomX) * 255);
|
|
90
|
+
rgb[i + 1] = Math.round((y / denomY) * 255);
|
|
91
|
+
rgb[i + 2] = 128;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const png = encodeRgbPng(width, height, rgb);
|
|
95
|
+
return {
|
|
96
|
+
base64: Buffer.from(png).toString("base64"),
|
|
97
|
+
mimeType: "image/png",
|
|
98
|
+
dimensions: { widthPx: width, heightPx: height },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const LARGE_TEXT_SAMPLE = "Aa Bb 123";
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* OSC 66 text-sizing sample lines, one scaled span per requested scale. Each
|
|
106
|
+
* scaled row is followed by `scale - 1` blank rows that reserve the vertical
|
|
107
|
+
* cells its multi-cell glyphs occupy — mirroring the markdown H1 renderer so
|
|
108
|
+
* the next line does not paint over the bottom of the glyphs.
|
|
109
|
+
*/
|
|
110
|
+
export function buildLargeTextLines(scales: readonly TextSizingScale[] = [2, 3]): string[] {
|
|
111
|
+
const lines: string[] = [];
|
|
112
|
+
for (const scale of scales) {
|
|
113
|
+
lines.push(` ${theme.fg("accent", encodeTextSized(`${LARGE_TEXT_SAMPLE} (${scale}x)`, { scale }))}`);
|
|
114
|
+
for (let reserved = 1; reserved < scale; reserved++) lines.push("");
|
|
115
|
+
}
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** HSV (h in degrees, s/v in 0..1) to 8-bit RGB, for the truecolor demo bar. */
|
|
120
|
+
function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
|
|
121
|
+
const c = v * s;
|
|
122
|
+
const hp = (((h % 360) + 360) % 360) / 60;
|
|
123
|
+
const x = c * (1 - Math.abs((hp % 2) - 1));
|
|
124
|
+
let r = 0;
|
|
125
|
+
let g = 0;
|
|
126
|
+
let b = 0;
|
|
127
|
+
if (hp < 1) [r, g, b] = [c, x, 0];
|
|
128
|
+
else if (hp < 2) [r, g, b] = [x, c, 0];
|
|
129
|
+
else if (hp < 3) [r, g, b] = [0, c, x];
|
|
130
|
+
else if (hp < 4) [r, g, b] = [0, x, c];
|
|
131
|
+
else if (hp < 5) [r, g, b] = [x, 0, c];
|
|
132
|
+
else [r, g, b] = [c, 0, x];
|
|
133
|
+
const m = v - c;
|
|
134
|
+
const to8 = (n: number) => Math.round((n + m) * 255);
|
|
135
|
+
return [to8(r), to8(g), to8(b)];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A 24-bit-color hue sweep rendered as background-painted cells (one space each). */
|
|
139
|
+
function truecolorBar(cells: number): string {
|
|
140
|
+
let out = "";
|
|
141
|
+
for (let i = 0; i < cells; i++) {
|
|
142
|
+
const [r, g, b] = hsvToRgb((i / cells) * 360, 0.85, 1);
|
|
143
|
+
out += `\x1b[48;2;${r};${g};${b}m `;
|
|
144
|
+
}
|
|
145
|
+
return `${out}\x1b[0m`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function notifyProtocolLabel(): string {
|
|
149
|
+
switch (TERMINAL.notifyProtocol) {
|
|
150
|
+
case NotifyProtocol.Osc99:
|
|
151
|
+
return "OSC 99 (kitty)";
|
|
152
|
+
case NotifyProtocol.Osc9:
|
|
153
|
+
return "OSC 9 (iTerm2/WezTerm)";
|
|
154
|
+
default:
|
|
155
|
+
return "BEL";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function imageProtocolLabel(): string {
|
|
160
|
+
switch (TERMINAL.imageProtocol) {
|
|
161
|
+
case ImageProtocol.Kitty:
|
|
162
|
+
return "Kitty graphics";
|
|
163
|
+
case ImageProtocol.Iterm2:
|
|
164
|
+
return "iTerm2 inline images";
|
|
165
|
+
case ImageProtocol.Sixel:
|
|
166
|
+
return "Sixel";
|
|
167
|
+
default:
|
|
168
|
+
return "none — text fallback";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Emits a fixed list of lines verbatim — no wrapping or padding. Used for OSC 66 spans. */
|
|
173
|
+
class RawLines implements Component {
|
|
174
|
+
#lines: readonly string[];
|
|
175
|
+
constructor(lines: readonly string[]) {
|
|
176
|
+
this.#lines = lines;
|
|
177
|
+
}
|
|
178
|
+
invalidate(): void {}
|
|
179
|
+
render(): string[] {
|
|
180
|
+
return [...this.#lines];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface ProtocolProbeOptions {
|
|
185
|
+
image: SampleImage;
|
|
186
|
+
imageBudget: ImageBudget;
|
|
187
|
+
/** Whether the desktop notification was suppressed (e.g. `PI_NOTIFICATIONS=off`). */
|
|
188
|
+
notificationSuppressed: boolean;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Self-contained panel that renders one sample of every special terminal
|
|
193
|
+
* protocol into the chat transcript.
|
|
194
|
+
*/
|
|
195
|
+
export class ProtocolProbeComponent extends Container {
|
|
196
|
+
constructor(options: ProtocolProbeOptions) {
|
|
197
|
+
super();
|
|
198
|
+
const hyperlinksOn = TERMINAL.hyperlinks;
|
|
199
|
+
const sizingOn = TERMINAL.textSizing;
|
|
200
|
+
const yesNo = (on: boolean) => (on ? theme.fg("success", "supported") : theme.fg("muted", "unsupported"));
|
|
201
|
+
|
|
202
|
+
this.addChild(new DynamicBorder());
|
|
203
|
+
this.addChild(new Text(theme.bold(theme.fg("accent", "Terminal Protocol Test")), 1, 0));
|
|
204
|
+
|
|
205
|
+
// Styling: SGR attributes, themed foregrounds, and a truecolor sweep.
|
|
206
|
+
const styling = [
|
|
207
|
+
theme.fg("muted", "Styling (SGR)"),
|
|
208
|
+
` ${theme.bold("bold")} ${theme.italic("italic")} ${theme.underline("underline")} ${theme.strikethrough("strike")} ${theme.inverse(" inverse ")} ${theme.fg("dim", "dim")}`,
|
|
209
|
+
` ${theme.fg("accent", "accent")} ${theme.fg("success", "success")} ${theme.fg("warning", "warning")} ${theme.fg("error", "error")}`,
|
|
210
|
+
` truecolor: ${truecolorBar(32)} (${theme.fg("muted", `24-bit ${TERMINAL.trueColor ? "on" : "off"}`)})`,
|
|
211
|
+
].join("\n");
|
|
212
|
+
this.addChild(new Text(styling, 1, 0));
|
|
213
|
+
this.addChild(new Spacer(1));
|
|
214
|
+
|
|
215
|
+
// Hyperlinks: OSC 8. Renders as plain text where unsupported.
|
|
216
|
+
this.addChild(
|
|
217
|
+
new Text(
|
|
218
|
+
[
|
|
219
|
+
`${theme.fg("muted", "Hyperlinks (OSC 8)")} — ${yesNo(hyperlinksOn)}`,
|
|
220
|
+
` \x1b]8;;https://github.com/can1357/oh-my-pi\x07oh-my-pi repo\x1b]8;;\x07`,
|
|
221
|
+
].join("\n"),
|
|
222
|
+
1,
|
|
223
|
+
0,
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
this.addChild(new Spacer(1));
|
|
227
|
+
|
|
228
|
+
// Text sizing: OSC 66.
|
|
229
|
+
this.addChild(new Text(`${theme.fg("muted", "Text sizing (OSC 66)")} — ${yesNo(sizingOn)}`, 1, 0));
|
|
230
|
+
if (sizingOn) {
|
|
231
|
+
this.addChild(new RawLines(buildLargeTextLines()));
|
|
232
|
+
} else {
|
|
233
|
+
this.addChild(
|
|
234
|
+
new Text(theme.fg("dim", " (enable via the tui.textSizing setting on a Kitty terminal)"), 1, 0),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
this.addChild(new Spacer(1));
|
|
238
|
+
|
|
239
|
+
// Graphics: Kitty / iTerm2 / Sixel, with a text fallback baked into Image.
|
|
240
|
+
this.addChild(new Text(`${theme.fg("muted", "Graphics")} — ${theme.fg("dim", imageProtocolLabel())}`, 1, 0));
|
|
241
|
+
this.addChild(
|
|
242
|
+
new Image(
|
|
243
|
+
options.image.base64,
|
|
244
|
+
options.image.mimeType,
|
|
245
|
+
{ fallbackColor: (text: string) => theme.fg("toolOutput", text) },
|
|
246
|
+
// Fixed modest caps (not the user's inline-image setting) keep the
|
|
247
|
+
// swatch a crisp, bounded preview rather than an upscaled wall.
|
|
248
|
+
{ maxWidthCells: 20, maxHeightCells: 16, budget: options.imageBudget },
|
|
249
|
+
options.image.dimensions,
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
this.addChild(new Spacer(1));
|
|
253
|
+
|
|
254
|
+
// Notifications: fired by the caller; this line reports the outcome.
|
|
255
|
+
const notifyStatus = options.notificationSuppressed
|
|
256
|
+
? theme.fg("warning", "suppressed (PI_NOTIFICATIONS)")
|
|
257
|
+
: theme.fg("success", "sent — check your desktop / titlebar");
|
|
258
|
+
this.addChild(
|
|
259
|
+
new Text(
|
|
260
|
+
`${theme.fg("muted", "Notification")} (${theme.fg("dim", notifyProtocolLabel())}) — ${notifyStatus}`,
|
|
261
|
+
1,
|
|
262
|
+
0,
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
this.addChild(new DynamicBorder());
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal state collection for the debug menu.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces the detected terminal, the established subprotocols the renderer
|
|
5
|
+
* negotiated (graphics, desktop notifications, hyperlinks, true color), the
|
|
6
|
+
* scrollback/erase strategy, and the live geometry — the details that decide
|
|
7
|
+
* which escape sequences the renderer emits.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
getCellDimensions,
|
|
11
|
+
ImageProtocol,
|
|
12
|
+
isOsc99Supported,
|
|
13
|
+
NotifyProtocol,
|
|
14
|
+
TERMINAL,
|
|
15
|
+
TERMINAL_ID,
|
|
16
|
+
} from "@oh-my-pi/pi-tui";
|
|
17
|
+
|
|
18
|
+
/** Live values the debug view reads off the running TUI, not the static capability table. */
|
|
19
|
+
export interface TerminalRuntimeState {
|
|
20
|
+
columns: number;
|
|
21
|
+
rows: number;
|
|
22
|
+
/** Whether DEC 2026 synchronized-output wrappers are currently emitted. */
|
|
23
|
+
synchronizedOutput: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TerminalStateInfo {
|
|
27
|
+
detectedId: string;
|
|
28
|
+
columns: number;
|
|
29
|
+
rows: number;
|
|
30
|
+
cellWidthPx: number;
|
|
31
|
+
cellHeightPx: number;
|
|
32
|
+
trueColor: boolean;
|
|
33
|
+
imageProtocol: string;
|
|
34
|
+
notifyProtocol: string;
|
|
35
|
+
osc99Confirmed: boolean;
|
|
36
|
+
hyperlinks: boolean;
|
|
37
|
+
deccara: boolean;
|
|
38
|
+
screenToScrollback: boolean;
|
|
39
|
+
eagerEraseScrollbackRisk: boolean;
|
|
40
|
+
synchronizedOutput: boolean;
|
|
41
|
+
multiplexer: string | null;
|
|
42
|
+
env: { TERM?: string; TERM_PROGRAM?: string; TERM_PROGRAM_VERSION?: string; COLORTERM?: string };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const IMAGE_PROTOCOL_NAMES: Record<ImageProtocol, string> = {
|
|
46
|
+
[ImageProtocol.Kitty]: "Kitty graphics",
|
|
47
|
+
[ImageProtocol.Iterm2]: "iTerm2 inline images",
|
|
48
|
+
[ImageProtocol.Sixel]: "Sixel",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const NOTIFY_PROTOCOL_NAMES: Record<NotifyProtocol, string> = {
|
|
52
|
+
[NotifyProtocol.Bell]: "BEL (\\a)",
|
|
53
|
+
[NotifyProtocol.Osc99]: "OSC 99 (kitty desktop notifications)",
|
|
54
|
+
[NotifyProtocol.Osc9]: "OSC 9 (iTerm2/WezTerm)",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Identify the multiplexer wrapping the session, if any (mirrors the renderer's gate). */
|
|
58
|
+
function detectMultiplexer(env: NodeJS.ProcessEnv): string | null {
|
|
59
|
+
if (env.TMUX) return "tmux";
|
|
60
|
+
if (env.STY) return "screen";
|
|
61
|
+
if (env.ZELLIJ) return "zellij";
|
|
62
|
+
const term = env.TERM?.toLowerCase() ?? "";
|
|
63
|
+
if (term.startsWith("tmux")) return "tmux";
|
|
64
|
+
if (term.startsWith("screen")) return "screen";
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Snapshot the active terminal capabilities and the live runtime geometry. */
|
|
69
|
+
export function collectTerminalState(runtime: TerminalRuntimeState): TerminalStateInfo {
|
|
70
|
+
const env = Bun.env;
|
|
71
|
+
const cell = getCellDimensions();
|
|
72
|
+
return {
|
|
73
|
+
detectedId: TERMINAL_ID,
|
|
74
|
+
columns: runtime.columns,
|
|
75
|
+
rows: runtime.rows,
|
|
76
|
+
cellWidthPx: cell.widthPx,
|
|
77
|
+
cellHeightPx: cell.heightPx,
|
|
78
|
+
trueColor: TERMINAL.trueColor,
|
|
79
|
+
imageProtocol: TERMINAL.imageProtocol === null ? "none" : IMAGE_PROTOCOL_NAMES[TERMINAL.imageProtocol],
|
|
80
|
+
notifyProtocol: NOTIFY_PROTOCOL_NAMES[TERMINAL.notifyProtocol],
|
|
81
|
+
osc99Confirmed: isOsc99Supported(),
|
|
82
|
+
hyperlinks: TERMINAL.hyperlinks,
|
|
83
|
+
deccara: TERMINAL.deccara,
|
|
84
|
+
screenToScrollback: TERMINAL.supportsScreenToScrollback,
|
|
85
|
+
eagerEraseScrollbackRisk: TERMINAL.eagerEraseScrollbackRisk,
|
|
86
|
+
synchronizedOutput: runtime.synchronizedOutput,
|
|
87
|
+
multiplexer: detectMultiplexer(env),
|
|
88
|
+
env: {
|
|
89
|
+
TERM: env.TERM,
|
|
90
|
+
TERM_PROGRAM: env.TERM_PROGRAM,
|
|
91
|
+
TERM_PROGRAM_VERSION: env.TERM_PROGRAM_VERSION,
|
|
92
|
+
COLORTERM: env.COLORTERM,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const yesNo = (value: boolean): string => (value ? "yes" : "no");
|
|
98
|
+
|
|
99
|
+
/** Format terminal state for display in the debug menu. */
|
|
100
|
+
export function formatTerminalState(info: TerminalStateInfo): string {
|
|
101
|
+
const lines = [
|
|
102
|
+
"Terminal State",
|
|
103
|
+
"━━━━━━━━━━━━━━",
|
|
104
|
+
`Detected: ${info.detectedId}`,
|
|
105
|
+
`Geometry: ${info.columns}x${info.rows} cells · cell ${info.cellWidthPx}x${info.cellHeightPx}px`,
|
|
106
|
+
info.multiplexer ? `Multiplexer: ${info.multiplexer}` : "Multiplexer: none",
|
|
107
|
+
"",
|
|
108
|
+
"Subprotocols",
|
|
109
|
+
` Graphics: ${info.imageProtocol}`,
|
|
110
|
+
` Notify: ${info.notifyProtocol}${info.osc99Confirmed ? " · confirmed via DA" : ""}`,
|
|
111
|
+
` Hyperlinks: ${yesNo(info.hyperlinks)} (OSC 8)`,
|
|
112
|
+
` True color: ${yesNo(info.trueColor)} (24-bit SGR)`,
|
|
113
|
+
` DECCARA: ${yesNo(info.deccara)} (rectangular-SGR background fills)`,
|
|
114
|
+
` Sync output: ${yesNo(info.synchronizedOutput)} (DEC 2026)`,
|
|
115
|
+
"",
|
|
116
|
+
"Scrollback",
|
|
117
|
+
` Screen->history clear: ${info.screenToScrollback ? "CSI 22 J" : "CSI 2 J (redraw)"}`,
|
|
118
|
+
` Eager-erase risk: ${yesNo(info.eagerEraseScrollbackRisk)} (ED3 may yank scrolled readers)`,
|
|
119
|
+
"",
|
|
120
|
+
"Detection signals",
|
|
121
|
+
` TERM: ${info.env.TERM ?? "(unset)"}`,
|
|
122
|
+
` TERM_PROGRAM: ${info.env.TERM_PROGRAM ?? "(unset)"}`,
|
|
123
|
+
` TERM_PROGRAM_VERSION: ${info.env.TERM_PROGRAM_VERSION ?? "(unset)"}`,
|
|
124
|
+
` COLORTERM: ${info.env.COLORTERM ?? "(unset)"}`,
|
|
125
|
+
];
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|