@oh-my-pi/pi-coding-agent 15.5.15 → 15.6.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 +46 -0
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/tiny-models-cli.d.ts +9 -0
- package/dist/types/commands/tiny-models.d.ts +22 -0
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +1 -1
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/config/model-id-affixes.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +232 -7
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
- package/dist/types/internal-urls/local-protocol.d.ts +2 -1
- package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
- package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
- package/dist/types/internal-urls/router.d.ts +8 -1
- package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
- package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
- package/dist/types/internal-urls/types.d.ts +26 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/resolve.d.ts +2 -1
- package/dist/types/memory-backend/types.d.ts +7 -1
- package/dist/types/mnemosyne/backend.d.ts +4 -0
- package/dist/types/mnemosyne/config.d.ts +29 -0
- package/dist/types/mnemosyne/index.d.ts +3 -0
- package/dist/types/mnemosyne/state.d.ts +72 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -3
- package/dist/types/modes/components/hook-selector.d.ts +27 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
- package/dist/types/modes/gradient-highlight.d.ts +23 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
- package/dist/types/modes/orchestrate.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
- package/dist/types/modes/ultrathink.d.ts +3 -3
- package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +33 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/render.d.ts +5 -1
- package/dist/types/tiny/models.d.ts +185 -0
- package/dist/types/tiny/text.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +24 -0
- package/dist/types/tiny/title-protocol.d.ts +74 -0
- package/dist/types/tiny/worker.d.ts +2 -0
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/index.d.ts +7 -3
- package/dist/types/tools/memory-edit.d.ts +40 -0
- package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
- package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
- package/dist/types/tools/memory-render.d.ts +60 -0
- package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
- package/dist/types/tools/todo-write.d.ts +8 -0
- package/dist/types/tools/tool-result.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +3 -0
- package/package.json +18 -14
- package/scripts/build-binary.ts +1 -0
- package/src/cli/tiny-models-cli.ts +127 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +8 -8
- package/src/commands/tiny-models.ts +36 -0
- package/src/config/model-equivalence.ts +43 -2
- package/src/config/model-id-affixes.ts +64 -0
- package/src/config/model-registry.ts +84 -10
- package/src/config/settings-schema.ts +205 -4
- package/src/edit/hashline/diff.ts +5 -7
- package/src/eval/__tests__/shared-executors.test.ts +36 -0
- package/src/eval/js/shared/local-module-loader.ts +13 -1
- package/src/eval/js/shared/rewrite-imports.ts +31 -26
- package/src/internal-urls/agent-protocol.ts +18 -1
- package/src/internal-urls/artifact-protocol.ts +19 -1
- package/src/internal-urls/docs-index.generated.ts +3 -1
- package/src/internal-urls/local-protocol.ts +14 -1
- package/src/internal-urls/memory-protocol.ts +6 -1
- package/src/internal-urls/omp-protocol.ts +5 -1
- package/src/internal-urls/router.ts +20 -1
- package/src/internal-urls/rule-protocol.ts +8 -1
- package/src/internal-urls/skill-protocol.ts +8 -1
- package/src/internal-urls/types.ts +27 -0
- package/src/lsp/render.ts +1 -1
- package/src/mcp/oauth-flow.ts +2 -2
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/resolve.ts +4 -1
- package/src/memory-backend/types.ts +8 -1
- package/src/mnemosyne/backend.ts +374 -0
- package/src/mnemosyne/config.ts +160 -0
- package/src/mnemosyne/index.ts +3 -0
- package/src/mnemosyne/state.ts +548 -0
- package/src/modes/acp/acp-agent.ts +11 -6
- package/src/modes/components/agent-dashboard.ts +4 -4
- package/src/modes/components/custom-editor.ts +3 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/extensions/extension-list.ts +3 -2
- package/src/modes/components/footer.ts +5 -6
- package/src/modes/components/history-search.ts +3 -3
- package/src/modes/components/hook-selector.ts +94 -8
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/mcp-add-wizard.ts +3 -3
- package/src/modes/components/model-selector.ts +5 -4
- package/src/modes/components/oauth-selector.ts +3 -3
- package/src/modes/components/session-observer-overlay.ts +19 -13
- package/src/modes/components/session-selector.ts +3 -3
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/status-line/context-thresholds.ts +11 -0
- package/src/modes/components/status-line/segments.ts +2 -2
- package/src/modes/components/tiny-title-download-progress.ts +90 -0
- package/src/modes/components/tips.txt +12 -0
- package/src/modes/components/tool-execution.ts +67 -3
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +3 -3
- package/src/modes/components/welcome.ts +55 -1
- package/src/modes/controllers/command-controller.ts +16 -1
- package/src/modes/controllers/extension-ui-controller.ts +3 -1
- package/src/modes/controllers/input-controller.ts +57 -0
- package/src/modes/gradient-highlight.ts +70 -0
- package/src/modes/interactive-mode.ts +58 -109
- package/src/modes/internal-url-autocomplete.ts +143 -0
- package/src/modes/orchestrate.ts +36 -0
- package/src/modes/prompt-action-autocomplete.ts +12 -0
- package/src/modes/ultrathink.ts +9 -53
- package/src/modes/utils/keybinding-matchers.ts +11 -0
- package/src/prompts/system/memory-consolidation-system.md +8 -0
- package/src/prompts/system/memory-extraction-system.md +26 -0
- package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +5 -16
- package/src/prompts/system/system-prompt.md +2 -0
- package/src/prompts/system/tiny-title-system.md +8 -0
- package/src/prompts/tools/memory-edit.md +8 -0
- package/src/prompts/tools/task.md +4 -7
- package/src/sdk.ts +8 -6
- package/src/session/agent-session.ts +128 -44
- package/src/slash-commands/builtin-registry.ts +10 -1
- package/src/system-prompt.ts +4 -0
- package/src/task/commands.ts +1 -5
- package/src/task/executor.ts +8 -0
- package/src/task/index.ts +2 -0
- package/src/task/render.ts +69 -26
- package/src/tiny/models.ts +217 -0
- package/src/tiny/text.ts +19 -0
- package/src/tiny/title-client.ts +340 -0
- package/src/tiny/title-protocol.ts +51 -0
- package/src/tiny/worker.ts +523 -0
- package/src/tools/bash.ts +58 -16
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/index.ts +17 -11
- package/src/tools/memory-edit.ts +59 -0
- package/src/tools/memory-recall.ts +100 -0
- package/src/tools/memory-reflect.ts +88 -0
- package/src/tools/memory-render.ts +185 -0
- package/src/tools/memory-retain.ts +91 -0
- package/src/tools/renderers.ts +4 -0
- package/src/tools/todo-write.ts +128 -29
- package/src/tools/tool-result.ts +8 -0
- package/src/utils/title-generator.ts +115 -13
- package/src/tools/hindsight-recall.ts +0 -69
- package/src/tools/hindsight-reflect.ts +0 -58
- package/src/tools/hindsight-retain.ts +0 -57
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { parentPort } from "node:worker_threads";
|
|
5
|
+
import type {
|
|
6
|
+
ProgressInfo,
|
|
7
|
+
TextGenerationPipeline,
|
|
8
|
+
TextGenerationStringOutput,
|
|
9
|
+
StoppingCriteria as TransformersStoppingCriteria,
|
|
10
|
+
} from "@huggingface/transformers";
|
|
11
|
+
import { getTinyModelsCacheDir, isCompiledBinary, prompt } from "@oh-my-pi/pi-utils";
|
|
12
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
13
|
+
import tinyTitleSystemPrompt from "../prompts/system/tiny-title-system.md" with { type: "text" };
|
|
14
|
+
import { getTinyLocalModelSpec, type TinyLocalModelKey, type TinyTitleLocalModelKey } from "./models";
|
|
15
|
+
import { formatTitleUserMessage, normalizeGeneratedTitle } from "./text";
|
|
16
|
+
import type {
|
|
17
|
+
TinyTitleProgressEvent,
|
|
18
|
+
TinyTitleTransport,
|
|
19
|
+
TinyTitleWorkerInbound,
|
|
20
|
+
TinyTitleWorkerOutbound,
|
|
21
|
+
} from "./title-protocol";
|
|
22
|
+
|
|
23
|
+
const TITLE_PREFILL = "<title>";
|
|
24
|
+
const TITLE_CLOSE = "</title>";
|
|
25
|
+
const TITLE_MAX_NEW_TOKENS = 20;
|
|
26
|
+
const STOP_DECODE_WINDOW_TOKENS = 32;
|
|
27
|
+
const MEMORY_COMPLETION_MAX_NEW_TOKENS = 256;
|
|
28
|
+
const TINY_TITLE_SYSTEM_PROMPT = prompt.render(tinyTitleSystemPrompt);
|
|
29
|
+
const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
|
|
30
|
+
const sourceRequire = createRequire(import.meta.url);
|
|
31
|
+
const INSTALL_LOCK_ATTEMPTS = 240;
|
|
32
|
+
const INSTALL_LOCK_SLEEP_MS = 250;
|
|
33
|
+
|
|
34
|
+
interface TransformersRuntime {
|
|
35
|
+
env: {
|
|
36
|
+
cacheDir?: string;
|
|
37
|
+
allowLocalModels?: boolean;
|
|
38
|
+
logLevel?: unknown;
|
|
39
|
+
};
|
|
40
|
+
LogLevel: {
|
|
41
|
+
ERROR: unknown;
|
|
42
|
+
};
|
|
43
|
+
StoppingCriteria: new () => TransformersStoppingCriteria;
|
|
44
|
+
pipeline: (
|
|
45
|
+
task: "text-generation",
|
|
46
|
+
model: string,
|
|
47
|
+
options: {
|
|
48
|
+
device: "cpu";
|
|
49
|
+
dtype: "q4";
|
|
50
|
+
progress_callback: (info: ProgressInfo) => void;
|
|
51
|
+
},
|
|
52
|
+
) => Promise<TextGenerationPipeline>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const pipelines = new Map<TinyLocalModelKey, Promise<TextGenerationPipeline>>();
|
|
56
|
+
|
|
57
|
+
function resolveTransformersVersionSpec(): string {
|
|
58
|
+
const manifest = packageJson as {
|
|
59
|
+
optionalDependencies?: Record<string, string>;
|
|
60
|
+
dependencies?: Record<string, string>;
|
|
61
|
+
};
|
|
62
|
+
const versionSpec =
|
|
63
|
+
manifest.optionalDependencies?.[TRANSFORMERS_PACKAGE] ?? manifest.dependencies?.[TRANSFORMERS_PACKAGE];
|
|
64
|
+
if (!versionSpec) throw new Error(`${TRANSFORMERS_PACKAGE} is missing from package.json optionalDependencies`);
|
|
65
|
+
if (!versionSpec.startsWith("catalog:")) return versionSpec;
|
|
66
|
+
const installed = sourceRequire(`${TRANSFORMERS_PACKAGE}/package.json`) as { version: string };
|
|
67
|
+
return installed.version;
|
|
68
|
+
}
|
|
69
|
+
let cachedTransformersVersionSpec: string | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Lazily resolve (and memoize) the transformers version spec. In the
|
|
72
|
+
* `catalog:` case {@link resolveTransformersVersionSpec} `require`s the
|
|
73
|
+
* installed `@huggingface/transformers/package.json`, so touching it forces
|
|
74
|
+
* the dependency to exist. Defer it to the compiled-binary runtime-install
|
|
75
|
+
* path — which only runs when a local title model is actually generated or
|
|
76
|
+
* downloaded — so loading this worker (smoke-test ping, online title path)
|
|
77
|
+
* never triggers the transformers resolve/install dance.
|
|
78
|
+
*/
|
|
79
|
+
function getTransformersVersionSpec(): string {
|
|
80
|
+
cachedTransformersVersionSpec ??= resolveTransformersVersionSpec();
|
|
81
|
+
return cachedTransformersVersionSpec;
|
|
82
|
+
}
|
|
83
|
+
function getTransformersRuntimeKey(): string {
|
|
84
|
+
return getTransformersVersionSpec().replace(/[^A-Za-z0-9._-]/g, "_");
|
|
85
|
+
}
|
|
86
|
+
let generateQueue = Promise.resolve();
|
|
87
|
+
let transformersRuntime: Promise<TransformersRuntime> | null = null;
|
|
88
|
+
|
|
89
|
+
function errorText(error: unknown): string {
|
|
90
|
+
return error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isErrnoCode(error: unknown, code: string): boolean {
|
|
94
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sendLog(
|
|
98
|
+
transport: TinyTitleTransport,
|
|
99
|
+
level: "debug" | "warn" | "error",
|
|
100
|
+
msg: string,
|
|
101
|
+
meta?: Record<string, unknown>,
|
|
102
|
+
): void {
|
|
103
|
+
transport.send({ type: "log", level, msg, meta });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getTinyTitleRuntimeDir(): string {
|
|
107
|
+
return path.join(
|
|
108
|
+
path.dirname(getTinyModelsCacheDir()),
|
|
109
|
+
"tiny-title-runtime",
|
|
110
|
+
`transformers-${getTransformersRuntimeKey()}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function acquireInstallLock(runtimeDir: string): Promise<() => Promise<void>> {
|
|
115
|
+
const lockDir = `${runtimeDir}.lock`;
|
|
116
|
+
for (let attempt = 0; attempt < INSTALL_LOCK_ATTEMPTS; attempt++) {
|
|
117
|
+
try {
|
|
118
|
+
await fs.mkdir(lockDir);
|
|
119
|
+
return async () => {
|
|
120
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (!isErrnoCode(error, "EEXIST")) throw error;
|
|
124
|
+
await Bun.sleep(INSTALL_LOCK_SLEEP_MS);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`Timed out waiting for tiny title runtime install lock: ${lockDir}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function isCompiledRuntimeInstalled(runtimeDir: string): Promise<boolean> {
|
|
131
|
+
return Bun.file(path.join(runtimeDir, "node_modules", "@huggingface", "transformers", "package.json")).exists();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function writeRuntimeManifest(runtimeDir: string): Promise<void> {
|
|
135
|
+
await fs.mkdir(runtimeDir, { recursive: true });
|
|
136
|
+
await Bun.write(
|
|
137
|
+
path.join(runtimeDir, "package.json"),
|
|
138
|
+
`${JSON.stringify(
|
|
139
|
+
{
|
|
140
|
+
private: true,
|
|
141
|
+
type: "module",
|
|
142
|
+
dependencies: {
|
|
143
|
+
[TRANSFORMERS_PACKAGE]: getTransformersVersionSpec(),
|
|
144
|
+
},
|
|
145
|
+
trustedDependencies: ["onnxruntime-node"],
|
|
146
|
+
},
|
|
147
|
+
null,
|
|
148
|
+
"\t",
|
|
149
|
+
)}\n`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function readPipe(stream: ReadableStream<Uint8Array> | null): Promise<string> {
|
|
154
|
+
if (!stream) return "";
|
|
155
|
+
return new Response(stream).text();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function runRuntimeInstall(runtimeDir: string): Promise<void> {
|
|
159
|
+
const proc = Bun.spawn([process.execPath, "install", "--cwd", runtimeDir, "--production"], {
|
|
160
|
+
env: { ...Bun.env, BUN_BE_BUN: "1" },
|
|
161
|
+
stdout: "pipe",
|
|
162
|
+
stderr: "pipe",
|
|
163
|
+
});
|
|
164
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
165
|
+
readPipe(proc.stdout as ReadableStream<Uint8Array> | null),
|
|
166
|
+
readPipe(proc.stderr as ReadableStream<Uint8Array> | null),
|
|
167
|
+
proc.exited,
|
|
168
|
+
]);
|
|
169
|
+
if (exitCode === 0) return;
|
|
170
|
+
const output = `${stdout}\n${stderr}`.trim();
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Failed to install tiny title runtime with ${process.execPath} install (exit ${exitCode}): ${output}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sendRuntimeInstallProgress(
|
|
177
|
+
transport: TinyTitleTransport,
|
|
178
|
+
requestId: string,
|
|
179
|
+
modelKey: TinyLocalModelKey,
|
|
180
|
+
status: "initiate" | "download" | "done",
|
|
181
|
+
): void {
|
|
182
|
+
transport.send({
|
|
183
|
+
type: "progress",
|
|
184
|
+
id: requestId,
|
|
185
|
+
event: {
|
|
186
|
+
modelKey,
|
|
187
|
+
status,
|
|
188
|
+
name: `${TRANSFORMERS_PACKAGE}@${getTransformersVersionSpec()}`,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function ensureCompiledTransformersRuntime(
|
|
194
|
+
transport: TinyTitleTransport,
|
|
195
|
+
requestId: string,
|
|
196
|
+
modelKey: TinyLocalModelKey,
|
|
197
|
+
): Promise<string> {
|
|
198
|
+
const runtimeDir = getTinyTitleRuntimeDir();
|
|
199
|
+
if (await isCompiledRuntimeInstalled(runtimeDir)) return runtimeDir;
|
|
200
|
+
|
|
201
|
+
sendRuntimeInstallProgress(transport, requestId, modelKey, "initiate");
|
|
202
|
+
const releaseLock = await acquireInstallLock(runtimeDir);
|
|
203
|
+
try {
|
|
204
|
+
if (await isCompiledRuntimeInstalled(runtimeDir)) return runtimeDir;
|
|
205
|
+
await writeRuntimeManifest(runtimeDir);
|
|
206
|
+
sendRuntimeInstallProgress(transport, requestId, modelKey, "download");
|
|
207
|
+
await runRuntimeInstall(runtimeDir);
|
|
208
|
+
sendRuntimeInstallProgress(transport, requestId, modelKey, "done");
|
|
209
|
+
return runtimeDir;
|
|
210
|
+
} finally {
|
|
211
|
+
await releaseLock();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function configureTransformers(transformers: TransformersRuntime): TransformersRuntime {
|
|
216
|
+
transformers.env.cacheDir = getTinyModelsCacheDir();
|
|
217
|
+
transformers.env.allowLocalModels = false;
|
|
218
|
+
transformers.env.logLevel = transformers.LogLevel.ERROR;
|
|
219
|
+
return transformers;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function loadTransformers(
|
|
223
|
+
transport: TinyTitleTransport,
|
|
224
|
+
requestId: string,
|
|
225
|
+
modelKey: TinyLocalModelKey,
|
|
226
|
+
): Promise<TransformersRuntime> {
|
|
227
|
+
if (transformersRuntime) return transformersRuntime;
|
|
228
|
+
transformersRuntime = (async () => {
|
|
229
|
+
if (!isCompiledBinary()) return configureTransformers(sourceRequire(TRANSFORMERS_PACKAGE) as TransformersRuntime);
|
|
230
|
+
const runtimeDir = await ensureCompiledTransformersRuntime(transport, requestId, modelKey);
|
|
231
|
+
const require_ = createRequire(path.join(runtimeDir, "package.json"));
|
|
232
|
+
return configureTransformers(require_(TRANSFORMERS_PACKAGE) as TransformersRuntime);
|
|
233
|
+
})().catch(error => {
|
|
234
|
+
transformersRuntime = null;
|
|
235
|
+
throw error;
|
|
236
|
+
});
|
|
237
|
+
return transformersRuntime;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function createStopOnTextCriteria(
|
|
241
|
+
transformers: TransformersRuntime,
|
|
242
|
+
tokenizer: TextGenerationPipeline["tokenizer"],
|
|
243
|
+
text: string,
|
|
244
|
+
): TransformersStoppingCriteria {
|
|
245
|
+
class StopOnTextCriteria extends transformers.StoppingCriteria {
|
|
246
|
+
#tokenizer: TextGenerationPipeline["tokenizer"];
|
|
247
|
+
#text: string;
|
|
248
|
+
|
|
249
|
+
constructor() {
|
|
250
|
+
super();
|
|
251
|
+
this.#tokenizer = tokenizer;
|
|
252
|
+
this.#text = text;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_call(inputIds: number[][]): boolean[] {
|
|
256
|
+
return inputIds.map(ids => {
|
|
257
|
+
const tail = ids.slice(-STOP_DECODE_WINDOW_TOKENS);
|
|
258
|
+
const decoded = this.#tokenizer.decode(tail, {
|
|
259
|
+
skip_special_tokens: false,
|
|
260
|
+
clean_up_tokenization_spaces: false,
|
|
261
|
+
});
|
|
262
|
+
return decoded.includes(this.#text);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return new StopOnTextCriteria();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function toProgressEvent(modelKey: TinyLocalModelKey, info: ProgressInfo): TinyTitleProgressEvent {
|
|
270
|
+
if (info.status === "ready") {
|
|
271
|
+
return { modelKey, status: info.status, task: info.task, model: info.model };
|
|
272
|
+
}
|
|
273
|
+
if (info.status === "progress_total") {
|
|
274
|
+
return {
|
|
275
|
+
modelKey,
|
|
276
|
+
status: info.status,
|
|
277
|
+
name: info.name,
|
|
278
|
+
progress: info.progress,
|
|
279
|
+
loaded: info.loaded,
|
|
280
|
+
total: info.total,
|
|
281
|
+
files: info.files,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (info.status === "progress") {
|
|
285
|
+
return {
|
|
286
|
+
modelKey,
|
|
287
|
+
status: info.status,
|
|
288
|
+
name: info.name,
|
|
289
|
+
file: info.file,
|
|
290
|
+
progress: info.progress,
|
|
291
|
+
loaded: info.loaded,
|
|
292
|
+
total: info.total,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
return { modelKey, status: info.status, name: info.name, file: info.file };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function sendProgress(
|
|
299
|
+
transport: TinyTitleTransport,
|
|
300
|
+
id: string,
|
|
301
|
+
modelKey: TinyLocalModelKey,
|
|
302
|
+
info: ProgressInfo,
|
|
303
|
+
): void {
|
|
304
|
+
transport.send({ type: "progress", id, event: toProgressEvent(modelKey, info) });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function loadPipeline(
|
|
308
|
+
modelKey: TinyLocalModelKey,
|
|
309
|
+
transport: TinyTitleTransport,
|
|
310
|
+
requestId: string,
|
|
311
|
+
): Promise<TextGenerationPipeline> {
|
|
312
|
+
const spec = getTinyLocalModelSpec(modelKey);
|
|
313
|
+
if (!spec) throw new Error(`Unknown tiny local model: ${modelKey}`);
|
|
314
|
+
const cached = pipelines.get(modelKey);
|
|
315
|
+
if (cached) {
|
|
316
|
+
void cached
|
|
317
|
+
.then(() => {
|
|
318
|
+
transport.send({
|
|
319
|
+
type: "progress",
|
|
320
|
+
id: requestId,
|
|
321
|
+
event: { modelKey, status: "ready", task: "text-generation", model: spec.repo },
|
|
322
|
+
});
|
|
323
|
+
})
|
|
324
|
+
.catch(() => undefined);
|
|
325
|
+
return cached;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const transformers = await loadTransformers(transport, requestId, modelKey);
|
|
329
|
+
const startedAt = performance.now();
|
|
330
|
+
const loaded = transformers
|
|
331
|
+
.pipeline("text-generation", spec.repo, {
|
|
332
|
+
device: "cpu",
|
|
333
|
+
dtype: spec.dtype,
|
|
334
|
+
progress_callback: info => sendProgress(transport, requestId, modelKey, info),
|
|
335
|
+
})
|
|
336
|
+
.then(
|
|
337
|
+
generator => {
|
|
338
|
+
sendLog(transport, "debug", "tiny-model: local model loaded", {
|
|
339
|
+
modelKey,
|
|
340
|
+
repo: spec.repo,
|
|
341
|
+
elapsedMs: Math.round(performance.now() - startedAt),
|
|
342
|
+
});
|
|
343
|
+
transport.send({
|
|
344
|
+
type: "progress",
|
|
345
|
+
id: requestId,
|
|
346
|
+
event: { modelKey, status: "ready", task: "text-generation", model: spec.repo },
|
|
347
|
+
});
|
|
348
|
+
return generator;
|
|
349
|
+
},
|
|
350
|
+
error => {
|
|
351
|
+
pipelines.delete(modelKey);
|
|
352
|
+
throw error;
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
pipelines.set(modelKey, loaded);
|
|
356
|
+
return loaded;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function buildPrompt(generator: TextGenerationPipeline, message: string): string {
|
|
360
|
+
const chat = [
|
|
361
|
+
{ role: "system", content: TINY_TITLE_SYSTEM_PROMPT },
|
|
362
|
+
{ role: "user", content: formatTitleUserMessage(message) },
|
|
363
|
+
];
|
|
364
|
+
const chatTemplateOptions = {
|
|
365
|
+
add_generation_prompt: true,
|
|
366
|
+
tokenize: false,
|
|
367
|
+
enable_thinking: false,
|
|
368
|
+
};
|
|
369
|
+
return `${generator.tokenizer.apply_chat_template(chat, chatTemplateOptions)}${TITLE_PREFILL}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function extractTinyTitle(text: string): string | null {
|
|
373
|
+
const titleStart = text.lastIndexOf(TITLE_PREFILL);
|
|
374
|
+
const withoutPrefix = titleStart >= 0 ? text.slice(titleStart + TITLE_PREFILL.length) : text;
|
|
375
|
+
const closeIndex = withoutPrefix.indexOf(TITLE_CLOSE);
|
|
376
|
+
const withoutClose = closeIndex >= 0 ? withoutPrefix.slice(0, closeIndex) : withoutPrefix;
|
|
377
|
+
const tagIndex = withoutClose.indexOf("<");
|
|
378
|
+
const withoutTag = tagIndex >= 0 ? withoutClose.slice(0, tagIndex) : withoutClose;
|
|
379
|
+
return normalizeGeneratedTitle(withoutTag);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function generateTitle(
|
|
383
|
+
transport: TinyTitleTransport,
|
|
384
|
+
requestId: string,
|
|
385
|
+
modelKey: TinyTitleLocalModelKey,
|
|
386
|
+
message: string,
|
|
387
|
+
): Promise<string | null> {
|
|
388
|
+
const generator = await loadPipeline(modelKey, transport, requestId);
|
|
389
|
+
const promptText = buildPrompt(generator, message);
|
|
390
|
+
const transformers = await loadTransformers(transport, requestId, modelKey);
|
|
391
|
+
const output = (await generator(promptText, {
|
|
392
|
+
max_new_tokens: TITLE_MAX_NEW_TOKENS,
|
|
393
|
+
do_sample: false,
|
|
394
|
+
return_full_text: false,
|
|
395
|
+
stopping_criteria: createStopOnTextCriteria(transformers, generator.tokenizer, TITLE_CLOSE),
|
|
396
|
+
})) as TextGenerationStringOutput;
|
|
397
|
+
return extractTinyTitle(output[0]?.generated_text ?? "");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function buildCompletionPrompt(generator: TextGenerationPipeline, promptText: string): string {
|
|
401
|
+
const chat = [{ role: "user", content: promptText }];
|
|
402
|
+
const chatTemplateOptions = {
|
|
403
|
+
add_generation_prompt: true,
|
|
404
|
+
tokenize: false,
|
|
405
|
+
enable_thinking: false,
|
|
406
|
+
};
|
|
407
|
+
return `${generator.tokenizer.apply_chat_template(chat, chatTemplateOptions)}`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Generic single-turn completion used by Mnemosyne memory tasks (fact extraction
|
|
412
|
+
* and consolidation). The caller (Mnemosyne) supplies the full task prompt; we
|
|
413
|
+
* wrap it as the user turn, decode greedily, and return the raw text for the
|
|
414
|
+
* caller's own parser. Output is capped to keep CPU latency bounded.
|
|
415
|
+
*/
|
|
416
|
+
async function generateCompletion(
|
|
417
|
+
transport: TinyTitleTransport,
|
|
418
|
+
requestId: string,
|
|
419
|
+
modelKey: TinyLocalModelKey,
|
|
420
|
+
promptText: string,
|
|
421
|
+
maxTokens: number | undefined,
|
|
422
|
+
): Promise<string | null> {
|
|
423
|
+
const generator = await loadPipeline(modelKey, transport, requestId);
|
|
424
|
+
const text = buildCompletionPrompt(generator, promptText);
|
|
425
|
+
const requested = maxTokens ?? MEMORY_COMPLETION_MAX_NEW_TOKENS;
|
|
426
|
+
const maxNewTokens = Math.min(Math.max(1, requested), MEMORY_COMPLETION_MAX_NEW_TOKENS);
|
|
427
|
+
const output = (await generator(text, {
|
|
428
|
+
max_new_tokens: maxNewTokens,
|
|
429
|
+
do_sample: false,
|
|
430
|
+
return_full_text: false,
|
|
431
|
+
})) as TextGenerationStringOutput;
|
|
432
|
+
const generated = (output[0]?.generated_text ?? "").trim();
|
|
433
|
+
return generated === "" ? null : generated;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function releasePipelines(): void {
|
|
437
|
+
// Intentionally NOT calling `pipeline.dispose()`. transformers.js disposes the
|
|
438
|
+
// underlying onnxruntime InferenceSession, freeing native memory that Bun's
|
|
439
|
+
// worker/NAPI teardown then frees a second time — a double-free that aborts the
|
|
440
|
+
// process on quit ("malloc: pointer being freed was not allocated" /
|
|
441
|
+
// "NAPI FATAL ERROR"). The worker is torn down immediately after `close`, so the
|
|
442
|
+
// OS reclaims the model memory regardless; skipping dispose avoids the crash.
|
|
443
|
+
pipelines.clear();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function enqueueRequest(
|
|
447
|
+
transport: TinyTitleTransport,
|
|
448
|
+
request: Extract<TinyTitleWorkerInbound, { type: "generate" | "complete" | "download" }>,
|
|
449
|
+
): void {
|
|
450
|
+
generateQueue = generateQueue.then(
|
|
451
|
+
async () => {
|
|
452
|
+
await handleQueuedRequest(transport, request);
|
|
453
|
+
},
|
|
454
|
+
async () => {
|
|
455
|
+
await handleQueuedRequest(transport, request);
|
|
456
|
+
},
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function handleQueuedRequest(
|
|
461
|
+
transport: TinyTitleTransport,
|
|
462
|
+
request: Extract<TinyTitleWorkerInbound, { type: "generate" | "complete" | "download" }>,
|
|
463
|
+
): Promise<void> {
|
|
464
|
+
try {
|
|
465
|
+
if (request.type === "download") {
|
|
466
|
+
await loadPipeline(request.modelKey, transport, request.id);
|
|
467
|
+
transport.send({ type: "downloaded", id: request.id });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (request.type === "complete") {
|
|
471
|
+
const text = await generateCompletion(
|
|
472
|
+
transport,
|
|
473
|
+
request.id,
|
|
474
|
+
request.modelKey,
|
|
475
|
+
request.prompt,
|
|
476
|
+
request.maxTokens,
|
|
477
|
+
);
|
|
478
|
+
transport.send({ type: "completion", id: request.id, text });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const title = await generateTitle(transport, request.id, request.modelKey, request.message);
|
|
482
|
+
transport.send({ type: "title", id: request.id, title });
|
|
483
|
+
} catch (error) {
|
|
484
|
+
transport.send({ type: "error", id: request.id, error: errorText(error) });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function startTinyTitleWorker(transport: TinyTitleTransport): void {
|
|
489
|
+
transport.onMessage(message => {
|
|
490
|
+
if (message.type === "ping") {
|
|
491
|
+
transport.send({ type: "pong", id: message.id });
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (message.type === "close") {
|
|
495
|
+
releasePipelines();
|
|
496
|
+
transport.send({ type: "closed" });
|
|
497
|
+
transport.close();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
enqueueRequest(transport, message);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (!parentPort) throw new Error("tiny-title-worker: missing parentPort");
|
|
505
|
+
|
|
506
|
+
const port = parentPort;
|
|
507
|
+
const transport: TinyTitleTransport = {
|
|
508
|
+
send: (message: TinyTitleWorkerOutbound) => port.postMessage(message),
|
|
509
|
+
onMessage: handler => {
|
|
510
|
+
const wrap = (data: unknown): void => handler(data as TinyTitleWorkerInbound);
|
|
511
|
+
port.on("message", wrap);
|
|
512
|
+
return () => port.off("message", wrap);
|
|
513
|
+
},
|
|
514
|
+
close: () => {
|
|
515
|
+
try {
|
|
516
|
+
port.close();
|
|
517
|
+
} catch {
|
|
518
|
+
// Already closed.
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
startTinyTitleWorker(transport);
|
package/src/tools/bash.ts
CHANGED
|
@@ -129,6 +129,8 @@ export interface BashToolDetails {
|
|
|
129
129
|
timeoutSeconds?: number;
|
|
130
130
|
requestedTimeoutSeconds?: number;
|
|
131
131
|
wallTimeMs?: number;
|
|
132
|
+
/** Exit code of a command that ran to completion but failed (non-zero). */
|
|
133
|
+
exitCode?: number;
|
|
132
134
|
terminalId?: string;
|
|
133
135
|
async?: {
|
|
134
136
|
state: "running" | "completed" | "failed";
|
|
@@ -281,17 +283,19 @@ function formatWallTimeNotice(wallTimeMs: number): string {
|
|
|
281
283
|
return `Wall time: ${formatWallTimeSeconds(wallTimeMs)} seconds`;
|
|
282
284
|
}
|
|
283
285
|
|
|
286
|
+
function formatExitCodeNotice(exitCode: number): string {
|
|
287
|
+
return `Command exited with code ${exitCode}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
284
290
|
/**
|
|
285
|
-
* Strip the trailing
|
|
286
|
-
* can
|
|
287
|
-
*
|
|
291
|
+
* Strip the trailing occurrence of `notice` (plus a single surrounding newline
|
|
292
|
+
* on each side) so the TUI can echo the value via a styled footer label
|
|
293
|
+
* instead of repeating it verbatim in the output pane. The notice is
|
|
294
|
+
* reconstructed from the same value the result was tagged with, so a literal
|
|
295
|
+
* sub-string match never strips a coincidental in-output token — only the
|
|
296
|
+
* exact line we appended in #buildCompletedResult.
|
|
288
297
|
*/
|
|
289
|
-
function
|
|
290
|
-
if (wallTimeMs === undefined) return text;
|
|
291
|
-
// Reconstruct the notice from the same value the result was tagged with so
|
|
292
|
-
// a literal sub-string match never strips a coincidental in-output token —
|
|
293
|
-
// only the exact line we appended in #buildCompletedResult.
|
|
294
|
-
const notice = formatWallTimeNotice(wallTimeMs);
|
|
298
|
+
function stripTrailingNotice(text: string, notice: string): string {
|
|
295
299
|
const idx = text.lastIndexOf(notice);
|
|
296
300
|
if (idx === -1) return text;
|
|
297
301
|
let start = idx;
|
|
@@ -301,6 +305,16 @@ function stripWallTimeNotice(text: string, wallTimeMs: number | undefined): stri
|
|
|
301
305
|
return (text.slice(0, start) + text.slice(end)).trimEnd();
|
|
302
306
|
}
|
|
303
307
|
|
|
308
|
+
function stripWallTimeNotice(text: string, wallTimeMs: number | undefined): string {
|
|
309
|
+
if (wallTimeMs === undefined) return text;
|
|
310
|
+
return stripTrailingNotice(text, formatWallTimeNotice(wallTimeMs));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function stripExitCodeNotice(text: string, exitCode: number | undefined): string {
|
|
314
|
+
if (exitCode === undefined) return text;
|
|
315
|
+
return stripTrailingNotice(text, formatExitCodeNotice(exitCode));
|
|
316
|
+
}
|
|
317
|
+
|
|
304
318
|
/**
|
|
305
319
|
* Bash tool implementation.
|
|
306
320
|
*
|
|
@@ -357,7 +371,15 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
357
371
|
return outputText || "(no output)";
|
|
358
372
|
}
|
|
359
373
|
|
|
360
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Throw for outcomes that are *not* a completed command: user/timeout
|
|
376
|
+
* aborts and a missing exit status. The foreground and bridge callers plus
|
|
377
|
+
* the async job manager rely on these throwing so cancellations surface as
|
|
378
|
+
* aborts and jobs are recorded as failed. A definite non-zero exit is a
|
|
379
|
+
* completed command that failed; #buildCompletedResult surfaces it as an
|
|
380
|
+
* error *result* (carrying execution details) rather than a throw.
|
|
381
|
+
*/
|
|
382
|
+
#throwIfUnfinished(result: BashResult | BashInteractiveResult, timeoutSec: number, outputText: string): void {
|
|
361
383
|
if (result.cancelled) {
|
|
362
384
|
throw new ToolError(normalizeResultOutput(result) || "Command aborted");
|
|
363
385
|
}
|
|
@@ -367,10 +389,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
367
389
|
if (result.exitCode === undefined) {
|
|
368
390
|
throw new ToolError(`${outputText}\n\nCommand failed: missing exit status`);
|
|
369
391
|
}
|
|
370
|
-
if (result.exitCode !== 0) {
|
|
371
|
-
throw new ToolError(`${outputText}\n\nCommand exited with code ${result.exitCode}`);
|
|
372
|
-
}
|
|
373
|
-
return outputText;
|
|
374
392
|
}
|
|
375
393
|
|
|
376
394
|
#buildCompletedResult(
|
|
@@ -383,6 +401,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
383
401
|
wallTimeMs?: number;
|
|
384
402
|
} = {},
|
|
385
403
|
): AgentToolResult<BashToolDetails> {
|
|
404
|
+
const exitCode = result.exitCode;
|
|
405
|
+
const failedExit = exitCode !== undefined && exitCode !== 0;
|
|
406
|
+
|
|
386
407
|
const outputLines = [this.#formatResultOutput(result)];
|
|
387
408
|
const notices: string[] = [];
|
|
388
409
|
if (options.wallTimeMs !== undefined) {
|
|
@@ -394,7 +415,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
394
415
|
}
|
|
395
416
|
}
|
|
396
417
|
if (notices.length > 0) outputLines.push("", ...notices);
|
|
418
|
+
if (failedExit) outputLines.push("", formatExitCodeNotice(exitCode));
|
|
397
419
|
const outputText = outputLines.join("\n");
|
|
420
|
+
|
|
421
|
+
// Aborts / timeouts / missing-status still propagate as thrown errors.
|
|
422
|
+
this.#throwIfUnfinished(result, timeoutSec, outputText);
|
|
423
|
+
|
|
398
424
|
const details: BashToolDetails = { timeoutSeconds: timeoutSec };
|
|
399
425
|
if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
|
|
400
426
|
details.requestedTimeoutSeconds = options.requestedTimeoutSec;
|
|
@@ -405,8 +431,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
405
431
|
if (options.wallTimeMs !== undefined) {
|
|
406
432
|
details.wallTimeMs = options.wallTimeMs;
|
|
407
433
|
}
|
|
434
|
+
if (failedExit) {
|
|
435
|
+
details.exitCode = exitCode;
|
|
436
|
+
}
|
|
408
437
|
const resultBuilder = toolResult(details).text(outputText).truncationFromSummary(result, { direction: "tail" });
|
|
409
|
-
|
|
438
|
+
if (failedExit) resultBuilder.error();
|
|
410
439
|
return resultBuilder.done();
|
|
411
440
|
}
|
|
412
441
|
|
|
@@ -500,7 +529,16 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
500
529
|
});
|
|
501
530
|
const finalText = this.#extractTextResult(finalResult);
|
|
502
531
|
latestText = finalText;
|
|
532
|
+
// Hand the detailed result to the foreground auto-background
|
|
533
|
+
// waiter (which renders it, footer included) before deciding
|
|
534
|
+
// the job's terminal state.
|
|
503
535
|
completion.resolve({ kind: "completed", result: finalResult });
|
|
536
|
+
if (finalResult.isError === true) {
|
|
537
|
+
// A non-zero exit is a completed command that failed. Re-enter
|
|
538
|
+
// the failure path so the job manager records it as failed and
|
|
539
|
+
// delivers the error text, matching prior throw-based behavior.
|
|
540
|
+
throw new ToolError(finalText);
|
|
541
|
+
}
|
|
504
542
|
await reportProgress(finalText, { async: { state: "completed", jobId, type: "bash" } });
|
|
505
543
|
return finalText;
|
|
506
544
|
} catch (error) {
|
|
@@ -1087,7 +1125,8 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1087
1125
|
// double-print it alongside the styled warning line below.
|
|
1088
1126
|
const rawOutput = renderContext?.output ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1089
1127
|
const strippedOutput = stripOutputNotice(rawOutput, details?.meta);
|
|
1090
|
-
const
|
|
1128
|
+
const withoutExit = stripExitCodeNotice(strippedOutput, details?.exitCode);
|
|
1129
|
+
const output = stripWallTimeNotice(withoutExit, details?.wallTimeMs);
|
|
1091
1130
|
const displayOutput = output.trimEnd();
|
|
1092
1131
|
const showingFullOutput = expanded && renderContext?.isFullOutput === true;
|
|
1093
1132
|
|
|
@@ -1106,6 +1145,9 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1106
1145
|
: `Timeout: ${timeoutSeconds}s`,
|
|
1107
1146
|
);
|
|
1108
1147
|
}
|
|
1148
|
+
if (isError && typeof details?.exitCode === "number") {
|
|
1149
|
+
statsParts.push(`Exit: ${details.exitCode}`);
|
|
1150
|
+
}
|
|
1109
1151
|
const timeoutLine =
|
|
1110
1152
|
statsParts.length > 0
|
|
1111
1153
|
? uiTheme.fg(
|
|
@@ -917,7 +917,7 @@ export class WorkerCore {
|
|
|
917
917
|
dispatchEvent: (event: unknown) => boolean;
|
|
918
918
|
}
|
|
919
919
|
const select = el as unknown as SelectLike;
|
|
920
|
-
if (
|
|
920
|
+
if (select?.tagName !== "SELECT") throw new Error("tab.select() requires a <select> element");
|
|
921
921
|
const EventCtor = (
|
|
922
922
|
globalThis as unknown as { Event: new (type: string, init?: { bubbles: boolean }) => unknown }
|
|
923
923
|
).Event;
|