@xynogen/pix-9router 0.6.0 → 0.7.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/README.md +6 -30
- package/package.json +4 -5
- package/src/data.ts +3 -3
- package/src/index.ts +4 -13
- package/src/catalog.ts +0 -45
- package/src/command.ts +0 -212
- package/src/defaults.ts +0 -44
- package/src/http.ts +0 -102
- package/src/recorder.ts +0 -103
- package/src/render.ts +0 -151
- package/src/stt-command.ts +0 -127
- package/src/transcribe.ts +0 -472
- package/src/tts.ts +0 -210
package/src/render.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* render.ts — shared dim renderers for fetch/search tool output.
|
|
3
|
-
*
|
|
4
|
-
* fetch/search return external web content the model already consumed; the
|
|
5
|
-
* full body is noise in the transcript. We render it dim (theme `dim` token)
|
|
6
|
-
* so it reads like faded reasoning — present for inspection, visually quiet.
|
|
7
|
-
* The tool title line stays normal so calls remain scannable.
|
|
8
|
-
*
|
|
9
|
-
* pi-tui's Text component is loaded via require() so the renderers degrade to
|
|
10
|
-
* the default pi renderer (undefined-safe via cast) when pi-tui is absent.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type {
|
|
14
|
-
AgentToolResult,
|
|
15
|
-
Theme,
|
|
16
|
-
ToolRenderResultOptions,
|
|
17
|
-
} from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
19
|
-
import {
|
|
20
|
-
type CollapsedToolStatus,
|
|
21
|
-
formatCollapsedToolRow,
|
|
22
|
-
frameToolResult,
|
|
23
|
-
hideCollapsedToolCall,
|
|
24
|
-
unframeToolResult,
|
|
25
|
-
} from "@xynogen/pix-pretty/utils";
|
|
26
|
-
import { type CollapseState, tickCollapse } from "@xynogen/pix-runtime/collapse";
|
|
27
|
-
|
|
28
|
-
interface TextLike extends Component {
|
|
29
|
-
setText(text: string): void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ToolRenderContext is not re-exported by pi-coding-agent; model the fields the
|
|
33
|
-
// renderers actually read. Structurally compatible with the SDK context.
|
|
34
|
-
interface RenderCtx {
|
|
35
|
-
lastComponent: Component | undefined;
|
|
36
|
-
isError: boolean;
|
|
37
|
-
state: Record<string, unknown>;
|
|
38
|
-
expanded: boolean;
|
|
39
|
-
invalidate: () => void;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface CompactRendererConfig<TDetails> {
|
|
43
|
-
tool: string;
|
|
44
|
-
target: (details: TDetails) => string;
|
|
45
|
-
meta: (details: TDetails) => string;
|
|
46
|
-
status?: (details: TDetails) => CollapsedToolStatus;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
type TextCtor = new (text?: string, padX?: number, padY?: number) => TextLike;
|
|
50
|
-
|
|
51
|
-
// Preview cap is intentionally generous — dimming already de-emphasises the
|
|
52
|
-
// body, so we only truncate to keep pathological multi-thousand-line dumps
|
|
53
|
-
// from flooding the viewport. Expanding shows everything.
|
|
54
|
-
const MAX_PREVIEW_LINES = 32;
|
|
55
|
-
|
|
56
|
-
let TextComponent: TextCtor | undefined;
|
|
57
|
-
try {
|
|
58
|
-
TextComponent = (require("@earendil-works/pi-tui") as { Text: TextCtor }).Text;
|
|
59
|
-
} catch {
|
|
60
|
-
TextComponent = undefined;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getText(lastComponent: Component | undefined): TextLike {
|
|
64
|
-
if (lastComponent && "setText" in lastComponent)
|
|
65
|
-
return unframeToolResult(lastComponent as TextLike);
|
|
66
|
-
// TextComponent is always present in the interactive TUI (pi-tui peer); the
|
|
67
|
-
// require() guard only matters for headless/test contexts where renderers
|
|
68
|
-
// are never invoked.
|
|
69
|
-
if (TextComponent) return new TextComponent("", 0, 0);
|
|
70
|
-
throw new Error("pi-tui Text component unavailable");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function allText(result: AgentToolResult<unknown>): string {
|
|
74
|
-
return (result.content ?? [])
|
|
75
|
-
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
76
|
-
.map((block) => block.text)
|
|
77
|
-
.join("\n");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function dimBody(body: string, theme: Theme, expanded: boolean): string {
|
|
81
|
-
const lines = body.split("\n");
|
|
82
|
-
const maxShow = expanded ? lines.length : MAX_PREVIEW_LINES;
|
|
83
|
-
const shown = lines.slice(0, maxShow);
|
|
84
|
-
const out = shown.map((line) => ` ${theme.fg("dim", line)}`);
|
|
85
|
-
const remaining = lines.length - maxShow;
|
|
86
|
-
if (remaining > 0) {
|
|
87
|
-
out.push(` ${theme.fg("muted", `… ${remaining} more lines`)}`);
|
|
88
|
-
}
|
|
89
|
-
return out.join("\n");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Build a `renderCall` that shows `<title> <secondary arg>`. */
|
|
93
|
-
export function makeRenderCall<TArgs>(title: string, pickArg: (args: TArgs) => string) {
|
|
94
|
-
return (args: TArgs, theme: Theme, ctx: RenderCtx): Component => {
|
|
95
|
-
const text = getText(ctx.lastComponent);
|
|
96
|
-
if (
|
|
97
|
-
hideCollapsedToolCall(ctx.state as CollapseState, ctx.expanded, (value) =>
|
|
98
|
-
text.setText(value),
|
|
99
|
-
)
|
|
100
|
-
)
|
|
101
|
-
return text;
|
|
102
|
-
const arg = pickArg(args);
|
|
103
|
-
text.setText(`${theme.fg("toolTitle", theme.bold(title))} ${theme.fg("dim", arg)}`);
|
|
104
|
-
return text;
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Build a `renderResult` with a bounded detail preview and optional compact terminal row. */
|
|
109
|
-
export function makeRenderResult<TDetails>(config?: CompactRendererConfig<TDetails>) {
|
|
110
|
-
return (
|
|
111
|
-
result: AgentToolResult<unknown>,
|
|
112
|
-
opts: ToolRenderResultOptions,
|
|
113
|
-
theme: Theme,
|
|
114
|
-
ctx: RenderCtx,
|
|
115
|
-
): Component => {
|
|
116
|
-
const text = getText(ctx.lastComponent);
|
|
117
|
-
const body = allText(result);
|
|
118
|
-
|
|
119
|
-
if (ctx.isError) {
|
|
120
|
-
text.setText(` ${theme.fg("error", body || "Error")}`);
|
|
121
|
-
return opts.isPartial ? text : frameToolResult(text, theme, true);
|
|
122
|
-
}
|
|
123
|
-
if (!body.trim()) {
|
|
124
|
-
text.setText(` ${theme.fg("muted", "(empty)")}`);
|
|
125
|
-
return opts.isPartial ? text : frameToolResult(text, theme, false);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const details = result.details as TDetails | undefined;
|
|
129
|
-
const isError = Boolean(details && config?.status?.(details) === "error");
|
|
130
|
-
if (
|
|
131
|
-
!opts.isPartial &&
|
|
132
|
-
config &&
|
|
133
|
-
details &&
|
|
134
|
-
tickCollapse(config.tool, ctx.state as CollapseState, ctx.invalidate, opts.expanded)
|
|
135
|
-
) {
|
|
136
|
-
text.setText(
|
|
137
|
-
formatCollapsedToolRow(
|
|
138
|
-
theme,
|
|
139
|
-
config.tool,
|
|
140
|
-
config.target(details),
|
|
141
|
-
config.meta(details),
|
|
142
|
-
config.status?.(details) ?? "success",
|
|
143
|
-
),
|
|
144
|
-
);
|
|
145
|
-
return text;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
text.setText(dimBody(body, theme, opts.expanded));
|
|
149
|
-
return opts.isPartial ? text : frameToolResult(text, theme, isError);
|
|
150
|
-
};
|
|
151
|
-
}
|
package/src/stt-command.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { matchesKey } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
4
|
-
frameModal,
|
|
5
|
-
modalOverlayOptions,
|
|
6
|
-
modalWidth,
|
|
7
|
-
terminalModalHeight,
|
|
8
|
-
} from "@xynogen/pix-pretty/modal-frame";
|
|
9
|
-
import { routerDefaults, saveDefaults } from "./defaults.js";
|
|
10
|
-
import { microphoneDevices, type Recording, startRecording } from "./recorder.js";
|
|
11
|
-
import { transcribeAudioFile } from "./transcribe.js";
|
|
12
|
-
|
|
13
|
-
function levelBar(db: number | undefined): string {
|
|
14
|
-
if (db === undefined) return "░".repeat(24);
|
|
15
|
-
const count = Math.max(0, Math.min(24, Math.round(((db + 60) / 60) * 24)));
|
|
16
|
-
return `${"█".repeat(count)}${"░".repeat(24 - count)}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function record(
|
|
20
|
-
ctx: ExtensionCommandContext,
|
|
21
|
-
devices: string[],
|
|
22
|
-
): Promise<string | undefined> {
|
|
23
|
-
return (
|
|
24
|
-
(await ctx.ui.custom<string | null>(
|
|
25
|
-
(tui, theme, _keybindings, done) => {
|
|
26
|
-
let deviceIndex = Math.max(0, devices.indexOf(routerDefaults.sttDevice));
|
|
27
|
-
let recording: Recording | undefined;
|
|
28
|
-
let level: number | undefined;
|
|
29
|
-
let error = "";
|
|
30
|
-
let stopping = false;
|
|
31
|
-
|
|
32
|
-
const stop = async () => {
|
|
33
|
-
if (!recording || stopping) return;
|
|
34
|
-
stopping = true;
|
|
35
|
-
try {
|
|
36
|
-
const current = recording;
|
|
37
|
-
await current.stop();
|
|
38
|
-
done(current.path);
|
|
39
|
-
} catch (cause) {
|
|
40
|
-
error = cause instanceof Error ? cause.message : String(cause);
|
|
41
|
-
recording = undefined;
|
|
42
|
-
stopping = false;
|
|
43
|
-
tui.requestRender();
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
render(width: number) {
|
|
49
|
-
const device = devices[deviceIndex] ?? "default";
|
|
50
|
-
return frameModal({
|
|
51
|
-
width: modalWidth(width),
|
|
52
|
-
maxHeight: terminalModalHeight(tui.terminal?.rows),
|
|
53
|
-
title: "Microphone",
|
|
54
|
-
titleColor: (text) => theme.fg("accent", theme.bold(text)),
|
|
55
|
-
header: [""],
|
|
56
|
-
body: [
|
|
57
|
-
`${theme.fg("dim", "input")} ${theme.fg("success", device)}`,
|
|
58
|
-
"",
|
|
59
|
-
`${theme.fg("dim", "level")} ${theme.fg(level !== undefined && level > -12 ? "warning" : "success", levelBar(level))} ${level === undefined ? "" : `${level.toFixed(1)} dB`}`,
|
|
60
|
-
"",
|
|
61
|
-
theme.fg(error ? "error" : "text", error || (recording ? "Recording…" : "Ready")),
|
|
62
|
-
],
|
|
63
|
-
footer: [
|
|
64
|
-
"",
|
|
65
|
-
theme.fg(
|
|
66
|
-
"muted",
|
|
67
|
-
recording
|
|
68
|
-
? "enter stop and transcribe · esc cancel"
|
|
69
|
-
: "←→ input · enter record · esc close",
|
|
70
|
-
),
|
|
71
|
-
],
|
|
72
|
-
color: (text) => theme.fg("accent", text),
|
|
73
|
-
bg: (text) => theme.bg("customMessageBg", text),
|
|
74
|
-
}).lines;
|
|
75
|
-
},
|
|
76
|
-
invalidate: () => {},
|
|
77
|
-
handleInput(data: string) {
|
|
78
|
-
if (matchesKey(data, "escape")) {
|
|
79
|
-
if (recording) void recording.stop().finally(() => done(null));
|
|
80
|
-
else done(null);
|
|
81
|
-
} else if (matchesKey(data, "enter")) {
|
|
82
|
-
if (recording) void stop();
|
|
83
|
-
else {
|
|
84
|
-
const device = devices[deviceIndex] ?? "default";
|
|
85
|
-
try {
|
|
86
|
-
recording = startRecording(device, (db) => {
|
|
87
|
-
level = db;
|
|
88
|
-
tui.requestRender();
|
|
89
|
-
});
|
|
90
|
-
routerDefaults.sttDevice = device;
|
|
91
|
-
saveDefaults(routerDefaults);
|
|
92
|
-
} catch (cause) {
|
|
93
|
-
error = cause instanceof Error ? cause.message : String(cause);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
} else if (!recording && (matchesKey(data, "left") || matchesKey(data, "right"))) {
|
|
97
|
-
const direction = matchesKey(data, "left") ? -1 : 1;
|
|
98
|
-
deviceIndex = (deviceIndex + direction + devices.length) % devices.length;
|
|
99
|
-
}
|
|
100
|
-
tui.requestRender();
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
},
|
|
104
|
-
{ overlay: true, overlayOptions: modalOverlayOptions() },
|
|
105
|
-
)) ?? undefined
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export default function registerSttCommand(pi: ExtensionAPI): void {
|
|
110
|
-
pi.registerCommand("stt", {
|
|
111
|
-
description: "Record microphone audio and put its transcript in the prompt",
|
|
112
|
-
handler: async (_args, ctx) => {
|
|
113
|
-
try {
|
|
114
|
-
const audio = await record(ctx, await microphoneDevices());
|
|
115
|
-
if (!audio) return;
|
|
116
|
-
ctx.ui.setStatus("9router-stt", "Transcribing microphone…");
|
|
117
|
-
const transcript = await transcribeAudioFile(audio);
|
|
118
|
-
ctx.ui.setEditorText(transcript);
|
|
119
|
-
ctx.ui.notify("The transcript is in the prompt editor.", "info");
|
|
120
|
-
} catch (error) {
|
|
121
|
-
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
122
|
-
} finally {
|
|
123
|
-
ctx.ui.setStatus("9router-stt", undefined);
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
}
|