@wrongstack/tui 0.3.1 → 0.3.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/dist/index.d.ts +4 -0
- package/dist/index.js +46 -120
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Agent, SlashCommandRegistry, AttachmentStore, EventBus, TokenCounter, QueueStore, Director } from '@wrongstack/core';
|
|
2
|
+
import { VisionAdapters } from '@wrongstack/runtime/vision';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
|
|
4
5
|
interface ProviderOption {
|
|
@@ -16,6 +17,9 @@ interface RunTuiOptions {
|
|
|
16
17
|
attachments: AttachmentStore;
|
|
17
18
|
events: EventBus;
|
|
18
19
|
tokenCounter?: TokenCounter;
|
|
20
|
+
visionAdapters?: VisionAdapters;
|
|
21
|
+
/** Resolve current model vision support. Falls back to provider capability when omitted. */
|
|
22
|
+
supportsVision?: () => boolean | Promise<boolean>;
|
|
19
23
|
model: string;
|
|
20
24
|
banner?: boolean;
|
|
21
25
|
/** Persists the input queue across crashes; if omitted, the queue is in-memory only. */
|
package/dist/index.js
CHANGED
|
@@ -1,117 +1,14 @@
|
|
|
1
1
|
import { render, useApp, Box, useStdout, Static, Text, useInput, useStdin } from 'ink';
|
|
2
2
|
import React, { useState, useReducer, useRef, useEffect, useMemo } from 'react';
|
|
3
|
-
import * as
|
|
4
|
-
import * as
|
|
3
|
+
import * as fs2 from 'fs/promises';
|
|
4
|
+
import * as path2 from 'path';
|
|
5
5
|
import { InputBuilder, formatTodosList } from '@wrongstack/core';
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
6
|
+
import { routeImagesForModel } from '@wrongstack/runtime/vision';
|
|
7
|
+
import { readClipboardImage } from '@wrongstack/runtime/clipboard';
|
|
8
8
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
9
10
|
|
|
10
11
|
// src/run-tui.ts
|
|
11
|
-
var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
12
|
-
async function readClipboardImage() {
|
|
13
|
-
const platform = process.platform;
|
|
14
|
-
if (platform === "win32") return readWindows();
|
|
15
|
-
if (platform === "darwin") return readDarwin();
|
|
16
|
-
if (platform === "linux") return readLinux();
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
async function readWindows() {
|
|
20
|
-
const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
|
|
21
|
-
const ps = [
|
|
22
|
-
"Add-Type -AssemblyName System.Windows.Forms",
|
|
23
|
-
"Add-Type -AssemblyName System.Drawing",
|
|
24
|
-
"$img = [System.Windows.Forms.Clipboard]::GetImage()",
|
|
25
|
-
'if ($img -eq $null) { Write-Output "NO_IMAGE"; exit 0 }',
|
|
26
|
-
`$img.Save('${tmp.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png)`,
|
|
27
|
-
'Write-Output "OK"'
|
|
28
|
-
].join("; ");
|
|
29
|
-
const out = await runCmd("powershell", ["-NoProfile", "-Command", ps]);
|
|
30
|
-
if (!out || out.trim() === "NO_IMAGE") return null;
|
|
31
|
-
if (!out.includes("OK")) return null;
|
|
32
|
-
return readPngFile(tmp);
|
|
33
|
-
}
|
|
34
|
-
async function readDarwin() {
|
|
35
|
-
const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
|
|
36
|
-
const script = [
|
|
37
|
-
"try",
|
|
38
|
-
` set the_file to (open for access POSIX file "${tmp}" with write permission)`,
|
|
39
|
-
" write (the clipboard as \xABclass PNGf\xBB) to the_file",
|
|
40
|
-
" close access the_file",
|
|
41
|
-
"on error",
|
|
42
|
-
" try",
|
|
43
|
-
' close access POSIX file "' + tmp + '"',
|
|
44
|
-
" end try",
|
|
45
|
-
' return "NO_IMAGE"',
|
|
46
|
-
"end try",
|
|
47
|
-
'return "OK"'
|
|
48
|
-
].join("\n");
|
|
49
|
-
const out = await runCmd("osascript", ["-e", script]);
|
|
50
|
-
if (!out || out.trim() !== "OK") return null;
|
|
51
|
-
return readPngFile(tmp);
|
|
52
|
-
}
|
|
53
|
-
async function readLinux() {
|
|
54
|
-
const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
|
|
55
|
-
const tries = [
|
|
56
|
-
["wl-paste", ["--type", "image/png"]],
|
|
57
|
-
["xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]]
|
|
58
|
-
];
|
|
59
|
-
for (const [cmd, args] of tries) {
|
|
60
|
-
const ok = await runCmdToFile(cmd, args, tmp).catch(() => false);
|
|
61
|
-
if (ok) return readPngFile(tmp);
|
|
62
|
-
}
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
async function readPngFile(p) {
|
|
66
|
-
try {
|
|
67
|
-
const buf = await fs.readFile(p);
|
|
68
|
-
if (buf.length === 0) {
|
|
69
|
-
await fs.unlink(p).catch(() => void 0);
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
if (buf.length > MAX_IMAGE_BYTES) {
|
|
73
|
-
await fs.unlink(p).catch(() => void 0);
|
|
74
|
-
throw new Error(`Clipboard image exceeds ${MAX_IMAGE_BYTES / 1024 / 1024}MB limit`);
|
|
75
|
-
}
|
|
76
|
-
if (buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71) {
|
|
77
|
-
await fs.unlink(p).catch(() => void 0);
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
await fs.unlink(p).catch(() => void 0);
|
|
81
|
-
return { base64: buf.toString("base64"), mediaType: "image/png", bytes: buf.length };
|
|
82
|
-
} catch (err) {
|
|
83
|
-
if (err.code === "ENOENT") return null;
|
|
84
|
-
throw err;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
function runCmd(cmd, args) {
|
|
88
|
-
return new Promise((resolve) => {
|
|
89
|
-
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
90
|
-
let out = "";
|
|
91
|
-
child.stdout.on("data", (c) => {
|
|
92
|
-
out += String(c);
|
|
93
|
-
});
|
|
94
|
-
child.on("error", () => resolve(null));
|
|
95
|
-
child.on("exit", (code) => resolve(code === 0 ? out : null));
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
function runCmdToFile(cmd, args, outPath) {
|
|
99
|
-
return new Promise((resolve) => {
|
|
100
|
-
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
101
|
-
const chunks = [];
|
|
102
|
-
child.stdout.on("data", (c) => chunks.push(c));
|
|
103
|
-
child.on("error", () => resolve(false));
|
|
104
|
-
child.on("exit", async (code) => {
|
|
105
|
-
if (code !== 0 || chunks.length === 0) return resolve(false);
|
|
106
|
-
try {
|
|
107
|
-
await fs.writeFile(outPath, Buffer.concat(chunks));
|
|
108
|
-
resolve(true);
|
|
109
|
-
} catch {
|
|
110
|
-
resolve(false);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
12
|
function stringifyInput(input) {
|
|
116
13
|
if (!input || typeof input !== "object") return "";
|
|
117
14
|
const obj = input;
|
|
@@ -214,8 +111,8 @@ function FilePicker({ query, matches, selected }) {
|
|
|
214
111
|
] }, m))
|
|
215
112
|
] });
|
|
216
113
|
}
|
|
217
|
-
function highlight(
|
|
218
|
-
return
|
|
114
|
+
function highlight(path3, query) {
|
|
115
|
+
return path3;
|
|
219
116
|
}
|
|
220
117
|
var STATUS_ICON = {
|
|
221
118
|
idle: { icon: "\u25CB", color: "gray" },
|
|
@@ -1974,10 +1871,10 @@ async function loadIndex(root) {
|
|
|
1974
1871
|
async function walk(root, rel, depth, out) {
|
|
1975
1872
|
if (out.length >= MAX_FILES_INDEXED) return;
|
|
1976
1873
|
if (depth > MAX_DEPTH) return;
|
|
1977
|
-
const dir = rel ?
|
|
1874
|
+
const dir = rel ? path2.join(root, rel) : root;
|
|
1978
1875
|
let entries;
|
|
1979
1876
|
try {
|
|
1980
|
-
entries = await
|
|
1877
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
1981
1878
|
} catch {
|
|
1982
1879
|
return;
|
|
1983
1880
|
}
|
|
@@ -2667,6 +2564,8 @@ function App({
|
|
|
2667
2564
|
attachments,
|
|
2668
2565
|
events,
|
|
2669
2566
|
tokenCounter,
|
|
2567
|
+
visionAdapters = [],
|
|
2568
|
+
supportsVision,
|
|
2670
2569
|
model,
|
|
2671
2570
|
banner = true,
|
|
2672
2571
|
queueStore,
|
|
@@ -2742,8 +2641,8 @@ function App({
|
|
|
2742
2641
|
const lastEnterAtRef = useRef(0);
|
|
2743
2642
|
const projectRoot = agent.ctx.projectRoot;
|
|
2744
2643
|
const projectName = React.useMemo(() => {
|
|
2745
|
-
const base =
|
|
2746
|
-
return base && base !==
|
|
2644
|
+
const base = path2.basename(projectRoot);
|
|
2645
|
+
return base && base !== path2.sep ? base : void 0;
|
|
2747
2646
|
}, [projectRoot]);
|
|
2748
2647
|
const streamingTextRef = useRef("");
|
|
2749
2648
|
const pendingDeltaRef = useRef("");
|
|
@@ -2853,7 +2752,7 @@ function App({
|
|
|
2853
2752
|
let cancelled = false;
|
|
2854
2753
|
const poll = async () => {
|
|
2855
2754
|
try {
|
|
2856
|
-
const data = await
|
|
2755
|
+
const data = await fs2.readFile(planPath, "utf8");
|
|
2857
2756
|
const parsed = JSON.parse(data);
|
|
2858
2757
|
if (cancelled) return;
|
|
2859
2758
|
if (!Array.isArray(parsed.items)) {
|
|
@@ -2985,9 +2884,9 @@ function App({
|
|
|
2985
2884
|
dispatch({ type: "pickerClose" });
|
|
2986
2885
|
return;
|
|
2987
2886
|
}
|
|
2988
|
-
const absPath =
|
|
2887
|
+
const absPath = path2.isAbsolute(picked) ? picked : path2.join(projectRoot, picked);
|
|
2989
2888
|
try {
|
|
2990
|
-
const data = await
|
|
2889
|
+
const data = await fs2.readFile(absPath, "utf8");
|
|
2991
2890
|
const placeholder = await builder.appendFile({
|
|
2992
2891
|
kind: "file",
|
|
2993
2892
|
data,
|
|
@@ -3900,7 +3799,24 @@ function App({
|
|
|
3900
3799
|
const startedAt = Date.now();
|
|
3901
3800
|
const before = tokenCounter?.total();
|
|
3902
3801
|
const costBefore = tokenCounter?.estimateCost().total ?? 0;
|
|
3903
|
-
const
|
|
3802
|
+
const routed = blocks.some((block) => block.type === "image") ? await routeImagesForModel(blocks, {
|
|
3803
|
+
supportsVision: supportsVision ? await supportsVision() : agent.ctx.provider.capabilities.vision,
|
|
3804
|
+
adapters: visionAdapters,
|
|
3805
|
+
ctx: agent.ctx,
|
|
3806
|
+
signal: ctrl.signal,
|
|
3807
|
+
providerId: agent.ctx.provider.id,
|
|
3808
|
+
model: agent.ctx.model
|
|
3809
|
+
}) : { blocks, route: "none", convertedImages: 0 };
|
|
3810
|
+
if (routed.route === "adapter") {
|
|
3811
|
+
dispatch({
|
|
3812
|
+
type: "addEntry",
|
|
3813
|
+
entry: {
|
|
3814
|
+
kind: "info",
|
|
3815
|
+
text: `Image input analyzed via ${routed.adapterName ?? "vision adapter"} (${routed.convertedImages} image${routed.convertedImages === 1 ? "" : "s"}).`
|
|
3816
|
+
}
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3819
|
+
const result = await agent.run(routed.blocks, { signal: ctrl.signal });
|
|
3904
3820
|
const lingering = streamingTextRef.current;
|
|
3905
3821
|
if (lingering.trim()) {
|
|
3906
3822
|
dispatch({ type: "addEntry", entry: { kind: "assistant", text: lingering } });
|
|
@@ -3953,11 +3869,19 @@ function App({
|
|
|
3953
3869
|
await runBlocks(head.blocks);
|
|
3954
3870
|
}
|
|
3955
3871
|
};
|
|
3872
|
+
const runBlocksRef = useRef(runBlocks);
|
|
3873
|
+
runBlocksRef.current = runBlocks;
|
|
3956
3874
|
const submit = async () => {
|
|
3957
3875
|
const raw = state.buffer;
|
|
3958
3876
|
const trimmed = raw.trim();
|
|
3959
3877
|
if (!trimmed && state.placeholders.length === 0) return;
|
|
3960
3878
|
dispatch({ type: "resetInterrupts" });
|
|
3879
|
+
if (trimmed === "/image" || trimmed === "/paste-image") {
|
|
3880
|
+
dispatch({ type: "clearInput" });
|
|
3881
|
+
await pasteClipboardImage();
|
|
3882
|
+
if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3961
3885
|
if (trimmed.startsWith("/")) {
|
|
3962
3886
|
dispatch({ type: "addEntry", entry: { kind: "user", text: trimmed } });
|
|
3963
3887
|
if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
|
|
@@ -4050,9 +3974,9 @@ function App({
|
|
|
4050
3974
|
b.appendText(ask);
|
|
4051
3975
|
}
|
|
4052
3976
|
const blocks = await b.submit();
|
|
4053
|
-
await
|
|
3977
|
+
await runBlocksRef.current(blocks);
|
|
4054
3978
|
})();
|
|
4055
|
-
}, []);
|
|
3979
|
+
}, [initialAsk, initialGoal]);
|
|
4056
3980
|
const inputHint = useMemo(() => {
|
|
4057
3981
|
if (state.status !== "idle") return "";
|
|
4058
3982
|
if (state.buffer.startsWith("/")) return "slash command \u2014 Enter to dispatch";
|
|
@@ -4253,6 +4177,8 @@ async function runTui(opts) {
|
|
|
4253
4177
|
attachments: opts.attachments,
|
|
4254
4178
|
events: opts.events,
|
|
4255
4179
|
tokenCounter: opts.tokenCounter,
|
|
4180
|
+
visionAdapters: opts.visionAdapters,
|
|
4181
|
+
supportsVision: opts.supportsVision,
|
|
4256
4182
|
model: opts.model,
|
|
4257
4183
|
banner: opts.banner ?? true,
|
|
4258
4184
|
queueStore: opts.queueStore,
|