@xynogen/pix-9router 0.4.0 → 0.4.2
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/package.json +2 -2
- package/src/fetch.ts +1 -3
- package/src/render.test.ts +51 -2
- package/src/render.ts +8 -4
- package/src/search.ts +1 -2
- package/src/transcribe.ts +1 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-9router",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Pi extension — 9Router provider + fetch/search tools via router API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@earendil-works/pi-tui": "*"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@xynogen/pix-pretty": "^1.
|
|
46
|
+
"@xynogen/pix-pretty": "^1.19.0",
|
|
47
47
|
"typebox": "^1.1.38",
|
|
48
48
|
"@xynogen/pix-runtime": "^0.8.0"
|
|
49
49
|
}
|
package/src/fetch.ts
CHANGED
|
@@ -206,9 +206,7 @@ export default function registerFetch(pi: ExtensionAPI): void {
|
|
|
206
206
|
promptSnippet:
|
|
207
207
|
"fetch(url, format, max_characters?) — format: 'markdown', 'text', or 'html'. Read page content via exa.",
|
|
208
208
|
promptGuidelines: [
|
|
209
|
-
"
|
|
210
|
-
"Prefer format='markdown' for readable content, 'text' for plain text extraction, 'html' for raw source.",
|
|
211
|
-
"Set max_characters to cap response size (default 1000, 0 = unlimited). Use 5000-10000 for typical pages.",
|
|
209
|
+
"fetch: format='markdown' for readable content, 'text' for plain extraction, 'html' for raw source. max_characters caps size (default 1000, 0 = unlimited; 5000-10000 for typical pages).",
|
|
212
210
|
],
|
|
213
211
|
renderCall: makeRenderCall<unknown>("fetch", (args) =>
|
|
214
212
|
String((args as Partial<FetchParams>).url ?? ""),
|
package/src/render.test.ts
CHANGED
|
@@ -13,7 +13,7 @@ function stubComponent() {
|
|
|
13
13
|
setText: (s: string) => {
|
|
14
14
|
captured = s;
|
|
15
15
|
},
|
|
16
|
-
render: () =>
|
|
16
|
+
render: (_width?: number) => captured.split("\n"),
|
|
17
17
|
invalidate: () => {},
|
|
18
18
|
};
|
|
19
19
|
return { component, get: () => captured };
|
|
@@ -89,13 +89,14 @@ test("partial updates keep detail visible and do not schedule collapse", () => {
|
|
|
89
89
|
context(stub, { state }),
|
|
90
90
|
);
|
|
91
91
|
expect(stub.get()).toContain("streaming body");
|
|
92
|
+
expect(stub.component.render(20)).toEqual([" [dim]streaming body"]);
|
|
92
93
|
expect(state.timer).toBeUndefined();
|
|
93
94
|
});
|
|
94
95
|
|
|
95
96
|
test("expanded mode restores full joined text", () => {
|
|
96
97
|
const stub = stubComponent();
|
|
97
98
|
const rr = makeRenderResult(config);
|
|
98
|
-
rr(
|
|
99
|
+
const component = rr(
|
|
99
100
|
{
|
|
100
101
|
content: [
|
|
101
102
|
{ type: "text", text: "first" },
|
|
@@ -109,6 +110,54 @@ test("expanded mode restores full joined text", () => {
|
|
|
109
110
|
context(stub, { state: { collapsed: true }, expanded: true }),
|
|
110
111
|
);
|
|
111
112
|
expect(stub.get()).toBe(" [dim]first\n [dim]second");
|
|
113
|
+
expect(component.render(20)).toEqual([
|
|
114
|
+
"[success]────────────────────",
|
|
115
|
+
" [dim]first",
|
|
116
|
+
" [dim]second",
|
|
117
|
+
"[success]────────────────────",
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("structured errors get red open frames but collapsed summaries stay unframed", () => {
|
|
122
|
+
const rr = makeRenderResult({ ...config, status: () => "error" as const });
|
|
123
|
+
const open = stubComponent();
|
|
124
|
+
const openComponent = rr(
|
|
125
|
+
{
|
|
126
|
+
content: [{ type: "text", text: "failed body" }],
|
|
127
|
+
details: { _type: "fetchResult", chars: 11 },
|
|
128
|
+
} as never,
|
|
129
|
+
{ expanded: false, isPartial: false },
|
|
130
|
+
theme,
|
|
131
|
+
context(open),
|
|
132
|
+
);
|
|
133
|
+
expect(openComponent.render(20)[0]).toBe("[error]────────────────────");
|
|
134
|
+
|
|
135
|
+
const collapsed = stubComponent();
|
|
136
|
+
const collapsedComponent = rr(
|
|
137
|
+
{
|
|
138
|
+
content: [{ type: "text", text: "failed body" }],
|
|
139
|
+
details: { _type: "fetchResult", chars: 11 },
|
|
140
|
+
} as never,
|
|
141
|
+
{ expanded: false, isPartial: false },
|
|
142
|
+
theme,
|
|
143
|
+
context(collapsed, { state: { collapsed: true } }),
|
|
144
|
+
);
|
|
145
|
+
expect(collapsedComponent.render(20).join("\n")).not.toContain("────");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("terminal error body gets an error frame", () => {
|
|
149
|
+
const stub = stubComponent();
|
|
150
|
+
const component = makeRenderResult(config)(
|
|
151
|
+
{ content: [{ type: "text", text: "boom" }] } as never,
|
|
152
|
+
{ expanded: true, isPartial: false },
|
|
153
|
+
theme,
|
|
154
|
+
context(stub, { isError: true, expanded: true }),
|
|
155
|
+
);
|
|
156
|
+
expect(component.render(20)).toEqual([
|
|
157
|
+
"[error]────────────────────",
|
|
158
|
+
" [error]boom",
|
|
159
|
+
"[error]────────────────────",
|
|
160
|
+
]);
|
|
112
161
|
});
|
|
113
162
|
|
|
114
163
|
test("renderResult caps the normal preview", () => {
|
package/src/render.ts
CHANGED
|
@@ -19,7 +19,9 @@ import type { Component } from "@earendil-works/pi-tui";
|
|
|
19
19
|
import {
|
|
20
20
|
type CollapsedToolStatus,
|
|
21
21
|
formatCollapsedToolRow,
|
|
22
|
+
frameToolResult,
|
|
22
23
|
hideCollapsedToolCall,
|
|
24
|
+
unframeToolResult,
|
|
23
25
|
} from "@xynogen/pix-pretty/utils";
|
|
24
26
|
import { type CollapseState, tickCollapse } from "@xynogen/pix-runtime/collapse";
|
|
25
27
|
|
|
@@ -59,7 +61,8 @@ try {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
function getText(lastComponent: Component | undefined): TextLike {
|
|
62
|
-
if (lastComponent && "setText" in lastComponent)
|
|
64
|
+
if (lastComponent && "setText" in lastComponent)
|
|
65
|
+
return unframeToolResult(lastComponent as TextLike);
|
|
63
66
|
// TextComponent is always present in the interactive TUI (pi-tui peer); the
|
|
64
67
|
// require() guard only matters for headless/test contexts where renderers
|
|
65
68
|
// are never invoked.
|
|
@@ -115,14 +118,15 @@ export function makeRenderResult<TDetails>(config?: CompactRendererConfig<TDetai
|
|
|
115
118
|
|
|
116
119
|
if (ctx.isError) {
|
|
117
120
|
text.setText(` ${theme.fg("error", body || "Error")}`);
|
|
118
|
-
return text;
|
|
121
|
+
return opts.isPartial ? text : frameToolResult(text, theme, true);
|
|
119
122
|
}
|
|
120
123
|
if (!body.trim()) {
|
|
121
124
|
text.setText(` ${theme.fg("muted", "(empty)")}`);
|
|
122
|
-
return text;
|
|
125
|
+
return opts.isPartial ? text : frameToolResult(text, theme, false);
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
const details = result.details as TDetails | undefined;
|
|
129
|
+
const isError = Boolean(details && config?.status?.(details) === "error");
|
|
126
130
|
if (
|
|
127
131
|
!opts.isPartial &&
|
|
128
132
|
config &&
|
|
@@ -142,6 +146,6 @@ export function makeRenderResult<TDetails>(config?: CompactRendererConfig<TDetai
|
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
text.setText(dimBody(body, theme, opts.expanded));
|
|
145
|
-
return text;
|
|
149
|
+
return opts.isPartial ? text : frameToolResult(text, theme, isError);
|
|
146
150
|
};
|
|
147
151
|
}
|
package/src/search.ts
CHANGED
|
@@ -232,8 +232,7 @@ export default function registerSearch(pi: ExtensionAPI): void {
|
|
|
232
232
|
promptSnippet:
|
|
233
233
|
"search(query, search_type, max_results?) — search_type: 'web' or 'news'. Defaults to 5 results, max 10.",
|
|
234
234
|
promptGuidelines: [
|
|
235
|
-
"
|
|
236
|
-
"Set search_type='web' for general web results, search_type='news' for recent news articles.",
|
|
235
|
+
"search: search_type='web' for general web results, 'news' for recent news articles.",
|
|
237
236
|
],
|
|
238
237
|
renderCall: makeRenderCall<unknown>("search", (args) =>
|
|
239
238
|
String((args as Partial<SearchParams>).query ?? ""),
|
package/src/transcribe.ts
CHANGED
|
@@ -340,14 +340,7 @@ export default function registerTranscribe(pi: ExtensionAPI): void {
|
|
|
340
340
|
isError: context.isError && options.expanded,
|
|
341
341
|
}),
|
|
342
342
|
promptGuidelines: [
|
|
343
|
-
"
|
|
344
|
-
"The file parameter should be an absolute or relative path to an audio file on disk.",
|
|
345
|
-
"Supports common audio formats: mp3, wav, flac, ogg, m4a, webm, mp4.",
|
|
346
|
-
"Default model is dg/nova-3 (Deepgram Nova 3). Override with model parameter if needed.",
|
|
347
|
-
"Optionally specify language as ISO 639-1 code (e.g. 'en', 'es', 'fr') for better accuracy.",
|
|
348
|
-
"Pass output_file to write the full transcription to disk — useful when the result is long, when it will be re-read or piped to another tool, or to keep chat context small. Relative paths are resolved against the current working directory.",
|
|
349
|
-
"output_file is pre-flight checked: paths under /etc, /proc, /sys, /boot, ~/.ssh, ~/.aws, ~/.gnupg, and ~/.config/gh are rejected; the target must not be a symlink or existing directory; the parent must exist and be writable; symlinks anywhere in the parent chain are rejected. On rejection, the transcription text is still returned inline so it is not lost.",
|
|
350
|
-
"Without output_file the transcribed text is returned inline (truncated to 50,000 chars).",
|
|
343
|
+
"transcribe: `language` as ISO 639-1 (e.g. 'en', 'es') improves accuracy. Pass `output_file` for long results — writes full text to disk (sensitive paths/symlinks rejected; text still returned inline on rejection) and returns a path summary. Without it, text returns inline truncated to 50,000 chars.",
|
|
351
344
|
],
|
|
352
345
|
parameters: Type.Object({
|
|
353
346
|
file: Type.String({
|