@aexol/spectral 0.7.6 → 0.7.8
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/dist/agent/index.js +16 -140
- package/dist/cli.js +25 -220
- package/dist/extensions/spectral-vision-fallback.js +188 -0
- package/dist/memory/commands/status.js +5 -5
- package/dist/memory/commands/view.js +16 -14
- package/dist/memory/compaction.js +31 -3
- package/dist/memory/prompts.js +5 -5
- package/dist/memory/tools/recall-observation.js +2 -2
- package/dist/pi/coding-agent/config.js +0 -11
- package/dist/pi/coding-agent/core/agent-session.js +3 -17
- package/dist/pi/coding-agent/core/extensions/loader.js +0 -6
- package/dist/pi/coding-agent/core/extensions/runner.js +7 -1
- package/dist/pi/coding-agent/core/keybindings.js +129 -2
- package/dist/pi/coding-agent/core/settings-manager.js +20 -0
- package/dist/pi/coding-agent/core/tools/bash.js +17 -63
- package/dist/pi/coding-agent/core/tools/edit.js +4 -141
- package/dist/pi/coding-agent/core/tools/find.js +0 -11
- package/dist/pi/coding-agent/core/tools/grep.js +0 -11
- package/dist/pi/coding-agent/core/tools/ls.js +0 -11
- package/dist/pi/coding-agent/core/tools/read.js +0 -12
- package/dist/pi/coding-agent/core/tools/render-utils.js +1 -14
- package/dist/pi/coding-agent/core/tools/write.js +2 -97
- package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +1 -1
- package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +6 -12
- package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1 -2
- package/dist/relay/models-fetch.js +13 -1
- package/dist/server/pi-bridge.js +57 -4
- package/dist/server/session-stream.js +7 -1
- package/package.json +1 -1
- package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +0 -248
- package/dist/pi/coding-agent/core/export-html/index.js +0 -225
- package/dist/pi/coding-agent/core/export-html/tool-renderer.js +0 -107
- package/dist/pi/tui/autocomplete.js +0 -631
- package/dist/pi/tui/components/box.js +0 -103
- package/dist/pi/tui/components/cancellable-loader.js +0 -34
- package/dist/pi/tui/components/editor.js +0 -1915
- package/dist/pi/tui/components/image.js +0 -88
- package/dist/pi/tui/components/input.js +0 -425
- package/dist/pi/tui/components/loader.js +0 -68
- package/dist/pi/tui/components/markdown.js +0 -633
- package/dist/pi/tui/components/select-list.js +0 -158
- package/dist/pi/tui/components/settings-list.js +0 -184
- package/dist/pi/tui/components/spacer.js +0 -22
- package/dist/pi/tui/components/text.js +0 -88
- package/dist/pi/tui/components/truncated-text.js +0 -50
- package/dist/pi/tui/editor-component.js +0 -1
- package/dist/pi/tui/fuzzy.js +0 -109
- package/dist/pi/tui/index.js +0 -31
- package/dist/pi/tui/keybindings.js +0 -173
- package/dist/pi/tui/keys.js +0 -1172
- package/dist/pi/tui/kill-ring.js +0 -43
- package/dist/pi/tui/stdin-buffer.js +0 -360
- package/dist/pi/tui/terminal-image.js +0 -335
- package/dist/pi/tui/terminal.js +0 -324
- package/dist/pi/tui/tui.js +0 -1076
- package/dist/pi/tui/undo-stack.js +0 -24
- package/dist/pi/tui/utils.js +0 -1016
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Container, Text } from "../../../tui/index.js";
|
|
2
1
|
import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises";
|
|
3
2
|
import { dirname } from "path";
|
|
4
3
|
import { Type } from "typebox";
|
|
@@ -16,73 +15,6 @@ const defaultWriteOperations = {
|
|
|
16
15
|
writeFile: (path, content) => fsWriteFile(path, content, "utf-8"),
|
|
17
16
|
mkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => { }),
|
|
18
17
|
};
|
|
19
|
-
class WriteCallRenderComponent extends Text {
|
|
20
|
-
cache;
|
|
21
|
-
constructor() {
|
|
22
|
-
super("", 0, 0);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const WRITE_PARTIAL_FULL_HIGHLIGHT_LINES = 50;
|
|
26
|
-
function highlightSingleLine(line, lang) {
|
|
27
|
-
const highlighted = highlightCode(line, lang);
|
|
28
|
-
return highlighted[0] ?? "";
|
|
29
|
-
}
|
|
30
|
-
function refreshWriteHighlightPrefix(cache) {
|
|
31
|
-
const prefixCount = Math.min(WRITE_PARTIAL_FULL_HIGHLIGHT_LINES, cache.normalizedLines.length);
|
|
32
|
-
if (prefixCount === 0)
|
|
33
|
-
return;
|
|
34
|
-
const prefixSource = cache.normalizedLines.slice(0, prefixCount).join("\n");
|
|
35
|
-
const prefixHighlighted = highlightCode(prefixSource, cache.lang);
|
|
36
|
-
for (let i = 0; i < prefixCount; i++) {
|
|
37
|
-
cache.highlightedLines[i] =
|
|
38
|
-
prefixHighlighted[i] ?? highlightSingleLine(cache.normalizedLines[i] ?? "", cache.lang);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function rebuildWriteHighlightCacheFull(rawPath, fileContent) {
|
|
42
|
-
const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
|
|
43
|
-
if (!lang)
|
|
44
|
-
return undefined;
|
|
45
|
-
const displayContent = normalizeDisplayText(fileContent);
|
|
46
|
-
const normalized = replaceTabs(displayContent);
|
|
47
|
-
return {
|
|
48
|
-
rawPath,
|
|
49
|
-
lang,
|
|
50
|
-
rawContent: fileContent,
|
|
51
|
-
normalizedLines: normalized.split("\n"),
|
|
52
|
-
highlightedLines: highlightCode(normalized, lang),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
function updateWriteHighlightCacheIncremental(cache, rawPath, fileContent) {
|
|
56
|
-
const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
|
|
57
|
-
if (!lang)
|
|
58
|
-
return undefined;
|
|
59
|
-
if (!cache)
|
|
60
|
-
return rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
|
61
|
-
if (cache.lang !== lang || cache.rawPath !== rawPath)
|
|
62
|
-
return rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
|
63
|
-
if (!fileContent.startsWith(cache.rawContent))
|
|
64
|
-
return rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
|
65
|
-
if (fileContent.length === cache.rawContent.length)
|
|
66
|
-
return cache;
|
|
67
|
-
const deltaRaw = fileContent.slice(cache.rawContent.length);
|
|
68
|
-
const deltaDisplay = normalizeDisplayText(deltaRaw);
|
|
69
|
-
const deltaNormalized = replaceTabs(deltaDisplay);
|
|
70
|
-
cache.rawContent = fileContent;
|
|
71
|
-
if (cache.normalizedLines.length === 0) {
|
|
72
|
-
cache.normalizedLines.push("");
|
|
73
|
-
cache.highlightedLines.push("");
|
|
74
|
-
}
|
|
75
|
-
const segments = deltaNormalized.split("\n");
|
|
76
|
-
const lastIndex = cache.normalizedLines.length - 1;
|
|
77
|
-
cache.normalizedLines[lastIndex] += segments[0];
|
|
78
|
-
cache.highlightedLines[lastIndex] = highlightSingleLine(cache.normalizedLines[lastIndex], cache.lang);
|
|
79
|
-
for (let i = 1; i < segments.length; i++) {
|
|
80
|
-
cache.normalizedLines.push(segments[i]);
|
|
81
|
-
cache.highlightedLines.push(highlightSingleLine(segments[i], cache.lang));
|
|
82
|
-
}
|
|
83
|
-
refreshWriteHighlightPrefix(cache);
|
|
84
|
-
return cache;
|
|
85
|
-
}
|
|
86
18
|
function trimTrailingEmptyLines(lines) {
|
|
87
19
|
let end = lines.length;
|
|
88
20
|
while (end > 0 && lines[end - 1] === "") {
|
|
@@ -90,7 +22,7 @@ function trimTrailingEmptyLines(lines) {
|
|
|
90
22
|
}
|
|
91
23
|
return lines.slice(0, end);
|
|
92
24
|
}
|
|
93
|
-
function formatWriteCall(args, options, theme
|
|
25
|
+
function formatWriteCall(args, options, theme) {
|
|
94
26
|
const rawPath = str(args?.file_path ?? args?.path);
|
|
95
27
|
const fileContent = str(args?.content);
|
|
96
28
|
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
@@ -102,7 +34,7 @@ function formatWriteCall(args, options, theme, cache) {
|
|
|
102
34
|
else if (fileContent) {
|
|
103
35
|
const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
|
|
104
36
|
const renderedLines = lang
|
|
105
|
-
?
|
|
37
|
+
? highlightCode(replaceTabs(normalizeDisplayText(fileContent)), lang)
|
|
106
38
|
: normalizeDisplayText(fileContent).split("\n");
|
|
107
39
|
const lines = trimTrailingEmptyLines(renderedLines);
|
|
108
40
|
const totalLines = lines.length;
|
|
@@ -178,33 +110,6 @@ export function createWriteToolDefinition(cwd, options) {
|
|
|
178
110
|
})();
|
|
179
111
|
}));
|
|
180
112
|
},
|
|
181
|
-
renderCall(args, theme, context) {
|
|
182
|
-
const renderArgs = args;
|
|
183
|
-
const rawPath = str(renderArgs?.file_path ?? renderArgs?.path);
|
|
184
|
-
const fileContent = str(renderArgs?.content);
|
|
185
|
-
const component = context.lastComponent ?? new WriteCallRenderComponent();
|
|
186
|
-
if (fileContent !== null) {
|
|
187
|
-
component.cache = context.argsComplete
|
|
188
|
-
? rebuildWriteHighlightCacheFull(rawPath, fileContent)
|
|
189
|
-
: updateWriteHighlightCacheIncremental(component.cache, rawPath, fileContent);
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
component.cache = undefined;
|
|
193
|
-
}
|
|
194
|
-
component.setText(formatWriteCall(renderArgs, { expanded: context.expanded, isPartial: context.isPartial }, theme, component.cache));
|
|
195
|
-
return component;
|
|
196
|
-
},
|
|
197
|
-
renderResult(result, _options, theme, context) {
|
|
198
|
-
const output = formatWriteResult({ ...result, isError: context.isError }, theme);
|
|
199
|
-
if (!output) {
|
|
200
|
-
const component = context.lastComponent ?? new Container();
|
|
201
|
-
component.clear();
|
|
202
|
-
return component;
|
|
203
|
-
}
|
|
204
|
-
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
205
|
-
text.setText(output);
|
|
206
|
-
return text;
|
|
207
|
-
},
|
|
208
113
|
};
|
|
209
114
|
}
|
|
210
115
|
export function createWriteTool(cwd, options) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utilities for formatting keybinding hints in the UI.
|
|
3
3
|
*/
|
|
4
|
-
import { getKeybindings } from "
|
|
4
|
+
import { getKeybindings } from "../../../core/keybindings.js";
|
|
5
5
|
import { theme } from "../theme/theme.js";
|
|
6
6
|
function formatKeyPart(part, options) {
|
|
7
7
|
const displayPart = process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part;
|
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared utility for truncating text to visual lines
|
|
3
|
-
*
|
|
2
|
+
* Shared utility for truncating text to visual lines.
|
|
3
|
+
* Simplified for headless mode — returns lines directly without Text component.
|
|
4
4
|
*/
|
|
5
|
-
import { Text } from "../../../../tui/index.js";
|
|
6
5
|
/**
|
|
7
6
|
* Truncate text to a maximum number of visual lines (from the end).
|
|
8
|
-
* This accounts for line wrapping based on terminal width.
|
|
9
7
|
*
|
|
10
8
|
* @param text - The text content (may contain newlines)
|
|
11
9
|
* @param maxVisualLines - Maximum number of visual lines to show
|
|
12
|
-
* @param
|
|
13
|
-
* @param
|
|
14
|
-
* Use 0 when result will be placed in a Box (Box adds its own padding).
|
|
15
|
-
* Use 1 when result will be placed in a plain Container.
|
|
10
|
+
* @param _width - Ignored in headless mode
|
|
11
|
+
* @param _paddingX - Ignored in headless mode
|
|
16
12
|
* @returns The truncated visual lines and count of skipped lines
|
|
17
13
|
*/
|
|
18
|
-
export function truncateToVisualLines(text, maxVisualLines,
|
|
14
|
+
export function truncateToVisualLines(text, maxVisualLines, _width, _paddingX = 0) {
|
|
19
15
|
if (!text) {
|
|
20
16
|
return { visualLines: [], skippedCount: 0 };
|
|
21
17
|
}
|
|
22
|
-
|
|
23
|
-
const tempText = new Text(text, paddingX, 0);
|
|
24
|
-
const allVisualLines = tempText.render(width);
|
|
18
|
+
const allVisualLines = text.split("\n");
|
|
25
19
|
if (allVisualLines.length <= maxVisualLines) {
|
|
26
20
|
return { visualLines: allVisualLines, skippedCount: 0 };
|
|
27
21
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getCapabilities, } from "../../../../tui/index.js";
|
|
4
3
|
import chalk from "chalk";
|
|
5
4
|
import { Type } from "typebox";
|
|
6
5
|
import { Compile } from "typebox/compile";
|
|
@@ -443,7 +442,7 @@ function loadThemeJson(name) {
|
|
|
443
442
|
return parseThemeJsonContent(name, content);
|
|
444
443
|
}
|
|
445
444
|
function createTheme(themeJson, mode, sourcePath) {
|
|
446
|
-
const colorMode = mode ??
|
|
445
|
+
const colorMode = mode ?? "256color";
|
|
447
446
|
const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);
|
|
448
447
|
const fgColors = {};
|
|
449
448
|
const bgColors = {};
|
|
@@ -28,7 +28,7 @@ const cache = new Map();
|
|
|
28
28
|
export function clearAllowedModelsCache() {
|
|
29
29
|
cache.clear();
|
|
30
30
|
}
|
|
31
|
-
const QUERY = `query AvailableBaseModels { availableBaseModels { name provider userModelId agentEnabled creditInputPer1M creditOutputPer1M creditCachedInputPer1M creditCacheReadPer1M creditCacheWritePer1M contextWindow supportsImages } }`;
|
|
31
|
+
const QUERY = `query AvailableBaseModels { availableBaseModels { name provider userModelId agentEnabled creditInputPer1M creditOutputPer1M creditCachedInputPer1M creditCacheReadPer1M creditCacheWritePer1M contextWindow supportsImages supportsReasoning isDefault isVisionDefault } }`;
|
|
32
32
|
/**
|
|
33
33
|
* Fetch the whitelist of allowed base models. Throws on any failure with a
|
|
34
34
|
* message tailored for an operator running `spectral serve` — the caller
|
|
@@ -104,6 +104,15 @@ export async function fetchAllowedModels(opts) {
|
|
|
104
104
|
const supportsImages = typeof row?.supportsImages === "boolean"
|
|
105
105
|
? row.supportsImages
|
|
106
106
|
: null;
|
|
107
|
+
const supportsReasoning = typeof row?.supportsReasoning === "boolean"
|
|
108
|
+
? row.supportsReasoning
|
|
109
|
+
: null;
|
|
110
|
+
const isDefault = typeof row?.isDefault === "boolean"
|
|
111
|
+
? row.isDefault
|
|
112
|
+
: null;
|
|
113
|
+
const isVisionDefault = typeof row?.isVisionDefault === "boolean"
|
|
114
|
+
? row.isVisionDefault
|
|
115
|
+
: null;
|
|
107
116
|
const model = {
|
|
108
117
|
modelId: name,
|
|
109
118
|
displayName: name,
|
|
@@ -115,6 +124,9 @@ export async function fetchAllowedModels(opts) {
|
|
|
115
124
|
creditCacheWritePer1M: asOptionalNumber(row?.creditCacheWritePer1M),
|
|
116
125
|
contextWindow,
|
|
117
126
|
supportsImages,
|
|
127
|
+
supportsReasoning,
|
|
128
|
+
isDefault,
|
|
129
|
+
isVisionDefault,
|
|
118
130
|
};
|
|
119
131
|
if (typeof row?.userModelId === "string") {
|
|
120
132
|
model.userModelId = row.userModelId;
|
package/dist/server/pi-bridge.js
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
* instance is reused across `prompt()` calls).
|
|
50
50
|
*/
|
|
51
51
|
import { createJiti } from "@mariozechner/jiti";
|
|
52
|
-
import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, } from "../pi/coding-agent/index.js";
|
|
52
|
+
import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, SettingsManager, } from "../pi/coding-agent/index.js";
|
|
53
53
|
import { randomUUID } from "node:crypto";
|
|
54
54
|
import { existsSync, statSync } from "node:fs";
|
|
55
55
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -219,6 +219,7 @@ const REASONING_SUPPORT_PREFIXES = [
|
|
|
219
219
|
"claude-opus-4",
|
|
220
220
|
"o3", "o4",
|
|
221
221
|
"deepseek-r1",
|
|
222
|
+
"deepseek-v4",
|
|
222
223
|
"gemini-2.5",
|
|
223
224
|
];
|
|
224
225
|
/** Check if a modelId prefix indicates reasoning/thinking support. */
|
|
@@ -226,6 +227,29 @@ function supportsReasoning(modelId) {
|
|
|
226
227
|
const bare = bareModelId(modelId);
|
|
227
228
|
return REASONING_SUPPORT_PREFIXES.some((p) => modelId.startsWith(p) || bare.startsWith(p));
|
|
228
229
|
}
|
|
230
|
+
function inferSyntheticOpenAICompat(model) {
|
|
231
|
+
const bare = bareModelId(model.modelId);
|
|
232
|
+
const isDeepSeek = model.provider === "deepseek" ||
|
|
233
|
+
model.modelId.startsWith("deepseek/") ||
|
|
234
|
+
bare.startsWith("deepseek");
|
|
235
|
+
if (isDeepSeek) {
|
|
236
|
+
return {
|
|
237
|
+
thinkingFormat: "deepseek",
|
|
238
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
239
|
+
supportsDeveloperRole: false,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (model.provider === "openrouter") {
|
|
243
|
+
const compat = {
|
|
244
|
+
thinkingFormat: "openrouter",
|
|
245
|
+
};
|
|
246
|
+
if (model.modelId.startsWith("anthropic/")) {
|
|
247
|
+
compat.cacheControlFormat = "anthropic";
|
|
248
|
+
}
|
|
249
|
+
return compat;
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
229
253
|
/**
|
|
230
254
|
* Calculate credits from token usage using per-model credit rates.
|
|
231
255
|
*
|
|
@@ -575,6 +599,29 @@ export class PiBridge {
|
|
|
575
599
|
}
|
|
576
600
|
this.allowedModels = allowedModels;
|
|
577
601
|
this.registerSyntheticProviders(allowedModels);
|
|
602
|
+
// Build an in-memory SettingsManager seeded with admin-configured
|
|
603
|
+
// defaults from the backend. findInitialModel() will pick up the
|
|
604
|
+
// isDefault model; the vision extension can query isVisionDefault.
|
|
605
|
+
const settingsOverrides = {};
|
|
606
|
+
const defaultModel = allowedModels.find((m) => m.isDefault);
|
|
607
|
+
if (defaultModel) {
|
|
608
|
+
const proxyProvider = defaultModel.provider === "anthropic"
|
|
609
|
+
? SPECTRAL_PROXY_ANTHROPIC
|
|
610
|
+
: SPECTRAL_PROXY_OPENAI;
|
|
611
|
+
settingsOverrides.defaultProvider = proxyProvider;
|
|
612
|
+
settingsOverrides.defaultModel = defaultModel.modelId;
|
|
613
|
+
console.info(`✓ Default model from backend: ${proxyProvider}/${defaultModel.modelId}`);
|
|
614
|
+
}
|
|
615
|
+
const defaultVisionModel = allowedModels.find((m) => m.isVisionDefault);
|
|
616
|
+
if (defaultVisionModel) {
|
|
617
|
+
const proxyProvider = defaultVisionModel.provider === "anthropic"
|
|
618
|
+
? SPECTRAL_PROXY_ANTHROPIC
|
|
619
|
+
: SPECTRAL_PROXY_OPENAI;
|
|
620
|
+
settingsOverrides.defaultVisionProvider = proxyProvider;
|
|
621
|
+
settingsOverrides.defaultVisionModel = defaultVisionModel.modelId;
|
|
622
|
+
console.info(`✓ Default vision model from backend: ${proxyProvider}/${defaultVisionModel.modelId}`);
|
|
623
|
+
}
|
|
624
|
+
const settingsManager = SettingsManager.inMemory(settingsOverrides);
|
|
578
625
|
console.info(`✓ Inference routed via backend proxy (${allowedModels.length} model(s) available)`);
|
|
579
626
|
const result = await createAgentSession({
|
|
580
627
|
cwd: this.opts.cwd,
|
|
@@ -582,6 +629,7 @@ export class PiBridge {
|
|
|
582
629
|
sessionManager,
|
|
583
630
|
authStorage,
|
|
584
631
|
modelRegistry: this.modelRegistry,
|
|
632
|
+
settingsManager,
|
|
585
633
|
});
|
|
586
634
|
this.session = result.session;
|
|
587
635
|
// Headless UI context: forwards extension notify() calls as wire events
|
|
@@ -649,7 +697,7 @@ export class PiBridge {
|
|
|
649
697
|
// at our synthetic proxy provider so auth resolves to the machine JWT.
|
|
650
698
|
provider: SPECTRAL_PROXY_ANTHROPIC,
|
|
651
699
|
baseUrl,
|
|
652
|
-
reasoning: supportsReasoning(m.modelId),
|
|
700
|
+
reasoning: m.supportsReasoning ?? supportsReasoning(m.modelId),
|
|
653
701
|
input: m.supportsImages !== false ? ["text", "image"] : ["text"],
|
|
654
702
|
// Real pricing so pi can compute accurate token costs.
|
|
655
703
|
cost: pricing
|
|
@@ -679,7 +727,7 @@ export class PiBridge {
|
|
|
679
727
|
// breaking auth lookup against our synthetic proxy provider.
|
|
680
728
|
provider: SPECTRAL_PROXY_OPENAI,
|
|
681
729
|
baseUrl,
|
|
682
|
-
reasoning: supportsReasoning(m.modelId),
|
|
730
|
+
reasoning: m.supportsReasoning ?? supportsReasoning(m.modelId),
|
|
683
731
|
input: m.supportsImages !== false ? ["text", "image"] : ["text"],
|
|
684
732
|
// Real pricing so pi can compute accurate token costs.
|
|
685
733
|
cost: pricing
|
|
@@ -687,6 +735,7 @@ export class PiBridge {
|
|
|
687
735
|
: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
688
736
|
contextWindow: m.contextWindow ?? 0,
|
|
689
737
|
maxTokens: 0,
|
|
738
|
+
compat: inferSyntheticOpenAICompat(m),
|
|
690
739
|
};
|
|
691
740
|
}),
|
|
692
741
|
});
|
|
@@ -710,13 +759,14 @@ export class PiBridge {
|
|
|
710
759
|
api: "openai-completions",
|
|
711
760
|
provider: SPECTRAL_PROXY_USER_MODEL,
|
|
712
761
|
baseUrl,
|
|
713
|
-
reasoning: supportsReasoning(m.modelId),
|
|
762
|
+
reasoning: m.supportsReasoning ?? supportsReasoning(m.modelId),
|
|
714
763
|
input: m.supportsImages !== false ? ["text", "image"] : ["text"],
|
|
715
764
|
cost: pricing
|
|
716
765
|
? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
|
|
717
766
|
: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
718
767
|
contextWindow: m.contextWindow ?? 0,
|
|
719
768
|
maxTokens: 0,
|
|
769
|
+
compat: inferSyntheticOpenAICompat(m),
|
|
720
770
|
};
|
|
721
771
|
}),
|
|
722
772
|
});
|
|
@@ -1177,6 +1227,8 @@ export class PiBridge {
|
|
|
1177
1227
|
const done = details.results.filter(r => r.exitCode !== undefined && r.exitCode !== -1).length;
|
|
1178
1228
|
const running = total - done;
|
|
1179
1229
|
const currentAgent = details.results.find(r => r.exitCode === -1 || r.exitCode === undefined)?.agent;
|
|
1230
|
+
// Extract streaming text from partialResult content (set by text_delta forwarding).
|
|
1231
|
+
const streamingText = extractTextFromContent(partialResult?.content);
|
|
1180
1232
|
const se = {
|
|
1181
1233
|
type: "subagent_progress",
|
|
1182
1234
|
toolCallId: ev.toolCallId,
|
|
@@ -1190,6 +1242,7 @@ export class PiBridge {
|
|
|
1190
1242
|
: mode === "chain"
|
|
1191
1243
|
? `Chain: step ${details.results.length}/${total}`
|
|
1192
1244
|
: `Running ${currentAgent ?? "subagent"}...`,
|
|
1245
|
+
streamingText: mode === "single" ? streamingText : undefined,
|
|
1193
1246
|
};
|
|
1194
1247
|
try {
|
|
1195
1248
|
this.opts.emit(se);
|
|
@@ -1173,6 +1173,10 @@ export class SessionStreamManager {
|
|
|
1173
1173
|
prunePersistedHistoryAfterCompaction(this.store, stream.sessionId, stream.bridge);
|
|
1174
1174
|
}
|
|
1175
1175
|
persistObservationalMemorySnapshot(this.store, stream.sessionId, stream.bridge);
|
|
1176
|
+
// Drain the prompt queue after compaction finishes. During
|
|
1177
|
+
// compaction, auto-dequeue in agent_end is skipped to prevent the
|
|
1178
|
+
// lossy prompt() guard from dropping queued items.
|
|
1179
|
+
this.maybeAutoDequeue(stream);
|
|
1176
1180
|
// After compaction the session context has been reduced; push updated
|
|
1177
1181
|
// context-window stats to all subscribers so the frontend's context
|
|
1178
1182
|
// bar refreshes immediately instead of waiting for the next turn.
|
|
@@ -1287,8 +1291,10 @@ export class SessionStreamManager {
|
|
|
1287
1291
|
// pending, check the persistent prompt queue. If there's a queued
|
|
1288
1292
|
// prompt, start it immediately without broadcasting agent_end —
|
|
1289
1293
|
// the frontend transitions seamlessly to the next turn.
|
|
1294
|
+
// Skip if compaction is in-flight (e.g. fork-compact just started
|
|
1295
|
+
// above) — compaction_end will drain the queue when it finishes.
|
|
1290
1296
|
if (!stream.loopActive || !stream.loopOriginalPrompt) {
|
|
1291
|
-
if (this.maybeAutoDequeue(stream))
|
|
1297
|
+
if (!stream.compacting && this.maybeAutoDequeue(stream))
|
|
1292
1298
|
return;
|
|
1293
1299
|
}
|
|
1294
1300
|
}
|
package/package.json
CHANGED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ANSI escape code to HTML converter.
|
|
3
|
-
*
|
|
4
|
-
* Converts terminal ANSI color/style codes to HTML with inline styles.
|
|
5
|
-
* Supports:
|
|
6
|
-
* - Standard foreground colors (30-37) and bright variants (90-97)
|
|
7
|
-
* - Standard background colors (40-47) and bright variants (100-107)
|
|
8
|
-
* - 256-color palette (38;5;N and 48;5;N)
|
|
9
|
-
* - RGB true color (38;2;R;G;B and 48;2;R;G;B)
|
|
10
|
-
* - Text styles: bold (1), dim (2), italic (3), underline (4)
|
|
11
|
-
* - Reset (0)
|
|
12
|
-
*/
|
|
13
|
-
// Standard ANSI color palette (0-15)
|
|
14
|
-
const ANSI_COLORS = [
|
|
15
|
-
"#000000", // 0: black
|
|
16
|
-
"#800000", // 1: red
|
|
17
|
-
"#008000", // 2: green
|
|
18
|
-
"#808000", // 3: yellow
|
|
19
|
-
"#000080", // 4: blue
|
|
20
|
-
"#800080", // 5: magenta
|
|
21
|
-
"#008080", // 6: cyan
|
|
22
|
-
"#c0c0c0", // 7: white
|
|
23
|
-
"#808080", // 8: bright black
|
|
24
|
-
"#ff0000", // 9: bright red
|
|
25
|
-
"#00ff00", // 10: bright green
|
|
26
|
-
"#ffff00", // 11: bright yellow
|
|
27
|
-
"#0000ff", // 12: bright blue
|
|
28
|
-
"#ff00ff", // 13: bright magenta
|
|
29
|
-
"#00ffff", // 14: bright cyan
|
|
30
|
-
"#ffffff", // 15: bright white
|
|
31
|
-
];
|
|
32
|
-
/**
|
|
33
|
-
* Convert 256-color index to hex.
|
|
34
|
-
*/
|
|
35
|
-
function color256ToHex(index) {
|
|
36
|
-
// Standard colors (0-15)
|
|
37
|
-
if (index < 16) {
|
|
38
|
-
return ANSI_COLORS[index];
|
|
39
|
-
}
|
|
40
|
-
// Color cube (16-231): 6x6x6 = 216 colors
|
|
41
|
-
if (index < 232) {
|
|
42
|
-
const cubeIndex = index - 16;
|
|
43
|
-
const r = Math.floor(cubeIndex / 36);
|
|
44
|
-
const g = Math.floor((cubeIndex % 36) / 6);
|
|
45
|
-
const b = cubeIndex % 6;
|
|
46
|
-
const toComponent = (n) => (n === 0 ? 0 : 55 + n * 40);
|
|
47
|
-
const toHex = (n) => toComponent(n).toString(16).padStart(2, "0");
|
|
48
|
-
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
49
|
-
}
|
|
50
|
-
// Grayscale (232-255): 24 shades
|
|
51
|
-
const gray = 8 + (index - 232) * 10;
|
|
52
|
-
const grayHex = gray.toString(16).padStart(2, "0");
|
|
53
|
-
return `#${grayHex}${grayHex}${grayHex}`;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Escape HTML special characters.
|
|
57
|
-
*/
|
|
58
|
-
function escapeHtml(text) {
|
|
59
|
-
return text
|
|
60
|
-
.replace(/&/g, "&")
|
|
61
|
-
.replace(/</g, "<")
|
|
62
|
-
.replace(/>/g, ">")
|
|
63
|
-
.replace(/"/g, """)
|
|
64
|
-
.replace(/'/g, "'");
|
|
65
|
-
}
|
|
66
|
-
function createEmptyStyle() {
|
|
67
|
-
return {
|
|
68
|
-
fg: null,
|
|
69
|
-
bg: null,
|
|
70
|
-
bold: false,
|
|
71
|
-
dim: false,
|
|
72
|
-
italic: false,
|
|
73
|
-
underline: false,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
function styleToInlineCSS(style) {
|
|
77
|
-
const parts = [];
|
|
78
|
-
if (style.fg)
|
|
79
|
-
parts.push(`color:${style.fg}`);
|
|
80
|
-
if (style.bg)
|
|
81
|
-
parts.push(`background-color:${style.bg}`);
|
|
82
|
-
if (style.bold)
|
|
83
|
-
parts.push("font-weight:bold");
|
|
84
|
-
if (style.dim)
|
|
85
|
-
parts.push("opacity:0.6");
|
|
86
|
-
if (style.italic)
|
|
87
|
-
parts.push("font-style:italic");
|
|
88
|
-
if (style.underline)
|
|
89
|
-
parts.push("text-decoration:underline");
|
|
90
|
-
return parts.join(";");
|
|
91
|
-
}
|
|
92
|
-
function hasStyle(style) {
|
|
93
|
-
return style.fg !== null || style.bg !== null || style.bold || style.dim || style.italic || style.underline;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Parse ANSI SGR (Select Graphic Rendition) codes and update style.
|
|
97
|
-
*/
|
|
98
|
-
function applySgrCode(params, style) {
|
|
99
|
-
let i = 0;
|
|
100
|
-
while (i < params.length) {
|
|
101
|
-
const code = params[i];
|
|
102
|
-
if (code === 0) {
|
|
103
|
-
// Reset all
|
|
104
|
-
style.fg = null;
|
|
105
|
-
style.bg = null;
|
|
106
|
-
style.bold = false;
|
|
107
|
-
style.dim = false;
|
|
108
|
-
style.italic = false;
|
|
109
|
-
style.underline = false;
|
|
110
|
-
}
|
|
111
|
-
else if (code === 1) {
|
|
112
|
-
style.bold = true;
|
|
113
|
-
}
|
|
114
|
-
else if (code === 2) {
|
|
115
|
-
style.dim = true;
|
|
116
|
-
}
|
|
117
|
-
else if (code === 3) {
|
|
118
|
-
style.italic = true;
|
|
119
|
-
}
|
|
120
|
-
else if (code === 4) {
|
|
121
|
-
style.underline = true;
|
|
122
|
-
}
|
|
123
|
-
else if (code === 22) {
|
|
124
|
-
// Reset bold/dim
|
|
125
|
-
style.bold = false;
|
|
126
|
-
style.dim = false;
|
|
127
|
-
}
|
|
128
|
-
else if (code === 23) {
|
|
129
|
-
style.italic = false;
|
|
130
|
-
}
|
|
131
|
-
else if (code === 24) {
|
|
132
|
-
style.underline = false;
|
|
133
|
-
}
|
|
134
|
-
else if (code >= 30 && code <= 37) {
|
|
135
|
-
// Standard foreground colors
|
|
136
|
-
style.fg = ANSI_COLORS[code - 30];
|
|
137
|
-
}
|
|
138
|
-
else if (code === 38) {
|
|
139
|
-
// Extended foreground color
|
|
140
|
-
if (params[i + 1] === 5 && params.length > i + 2) {
|
|
141
|
-
// 256-color: 38;5;N
|
|
142
|
-
style.fg = color256ToHex(params[i + 2]);
|
|
143
|
-
i += 2;
|
|
144
|
-
}
|
|
145
|
-
else if (params[i + 1] === 2 && params.length > i + 4) {
|
|
146
|
-
// RGB: 38;2;R;G;B
|
|
147
|
-
const r = params[i + 2];
|
|
148
|
-
const g = params[i + 3];
|
|
149
|
-
const b = params[i + 4];
|
|
150
|
-
style.fg = `rgb(${r},${g},${b})`;
|
|
151
|
-
i += 4;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
else if (code === 39) {
|
|
155
|
-
// Default foreground
|
|
156
|
-
style.fg = null;
|
|
157
|
-
}
|
|
158
|
-
else if (code >= 40 && code <= 47) {
|
|
159
|
-
// Standard background colors
|
|
160
|
-
style.bg = ANSI_COLORS[code - 40];
|
|
161
|
-
}
|
|
162
|
-
else if (code === 48) {
|
|
163
|
-
// Extended background color
|
|
164
|
-
if (params[i + 1] === 5 && params.length > i + 2) {
|
|
165
|
-
// 256-color: 48;5;N
|
|
166
|
-
style.bg = color256ToHex(params[i + 2]);
|
|
167
|
-
i += 2;
|
|
168
|
-
}
|
|
169
|
-
else if (params[i + 1] === 2 && params.length > i + 4) {
|
|
170
|
-
// RGB: 48;2;R;G;B
|
|
171
|
-
const r = params[i + 2];
|
|
172
|
-
const g = params[i + 3];
|
|
173
|
-
const b = params[i + 4];
|
|
174
|
-
style.bg = `rgb(${r},${g},${b})`;
|
|
175
|
-
i += 4;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
else if (code === 49) {
|
|
179
|
-
// Default background
|
|
180
|
-
style.bg = null;
|
|
181
|
-
}
|
|
182
|
-
else if (code >= 90 && code <= 97) {
|
|
183
|
-
// Bright foreground colors
|
|
184
|
-
style.fg = ANSI_COLORS[code - 90 + 8];
|
|
185
|
-
}
|
|
186
|
-
else if (code >= 100 && code <= 107) {
|
|
187
|
-
// Bright background colors
|
|
188
|
-
style.bg = ANSI_COLORS[code - 100 + 8];
|
|
189
|
-
}
|
|
190
|
-
// Ignore unrecognized codes
|
|
191
|
-
i++;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// Match ANSI escape sequences: ESC[ followed by params and ending with 'm'
|
|
195
|
-
const ANSI_REGEX = /\x1b\[([\d;]*)m/g;
|
|
196
|
-
/**
|
|
197
|
-
* Convert ANSI-escaped text to HTML with inline styles.
|
|
198
|
-
*/
|
|
199
|
-
export function ansiToHtml(text) {
|
|
200
|
-
const style = createEmptyStyle();
|
|
201
|
-
let result = "";
|
|
202
|
-
let lastIndex = 0;
|
|
203
|
-
let inSpan = false;
|
|
204
|
-
// Reset regex state
|
|
205
|
-
ANSI_REGEX.lastIndex = 0;
|
|
206
|
-
let match = ANSI_REGEX.exec(text);
|
|
207
|
-
while (match !== null) {
|
|
208
|
-
// Add text before this escape sequence
|
|
209
|
-
const beforeText = text.slice(lastIndex, match.index);
|
|
210
|
-
if (beforeText) {
|
|
211
|
-
result += escapeHtml(beforeText);
|
|
212
|
-
}
|
|
213
|
-
// Parse SGR parameters
|
|
214
|
-
const paramStr = match[1];
|
|
215
|
-
const params = paramStr ? paramStr.split(";").map((p) => parseInt(p, 10) || 0) : [0];
|
|
216
|
-
// Close existing span if we have one
|
|
217
|
-
if (inSpan) {
|
|
218
|
-
result += "</span>";
|
|
219
|
-
inSpan = false;
|
|
220
|
-
}
|
|
221
|
-
// Apply the codes
|
|
222
|
-
applySgrCode(params, style);
|
|
223
|
-
// Open new span if we have any styling
|
|
224
|
-
if (hasStyle(style)) {
|
|
225
|
-
result += `<span style="${styleToInlineCSS(style)}">`;
|
|
226
|
-
inSpan = true;
|
|
227
|
-
}
|
|
228
|
-
lastIndex = match.index + match[0].length;
|
|
229
|
-
match = ANSI_REGEX.exec(text);
|
|
230
|
-
}
|
|
231
|
-
// Add remaining text
|
|
232
|
-
const remainingText = text.slice(lastIndex);
|
|
233
|
-
if (remainingText) {
|
|
234
|
-
result += escapeHtml(remainingText);
|
|
235
|
-
}
|
|
236
|
-
// Close any open span
|
|
237
|
-
if (inSpan) {
|
|
238
|
-
result += "</span>";
|
|
239
|
-
}
|
|
240
|
-
return result;
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Convert array of ANSI-escaped lines to HTML.
|
|
244
|
-
* Each line is wrapped in a div element.
|
|
245
|
-
*/
|
|
246
|
-
export function ansiLinesToHtml(lines) {
|
|
247
|
-
return lines.map((line) => `<div class="ansi-line">${ansiToHtml(line) || " "}</div>`).join("");
|
|
248
|
-
}
|