@tabbio-technologies/cli 1.2.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/README.md +763 -0
- package/dist/chunks/chunk-7LNC3FIV.js +2019 -0
- package/dist/chunks/chunk-7LNC3FIV.js.map +7 -0
- package/dist/chunks/chunk-NAWA6NCZ.js +259 -0
- package/dist/chunks/chunk-NAWA6NCZ.js.map +7 -0
- package/dist/chunks/chunk-NK5RPNSV.js +15 -0
- package/dist/chunks/chunk-NK5RPNSV.js.map +7 -0
- package/dist/chunks/chunk-QVR5KIEQ.js +1976 -0
- package/dist/chunks/chunk-QVR5KIEQ.js.map +7 -0
- package/dist/chunks/chunk-VHHZFMIF.js +162 -0
- package/dist/chunks/chunk-VHHZFMIF.js.map +7 -0
- package/dist/chunks/chunk-Y6GWIFHI.js +227 -0
- package/dist/chunks/chunk-Y6GWIFHI.js.map +7 -0
- package/dist/chunks/entry-WENOOT6W.js +3492 -0
- package/dist/chunks/entry-WENOOT6W.js.map +7 -0
- package/dist/chunks/mount-NGUARMCL.js +79 -0
- package/dist/chunks/mount-NGUARMCL.js.map +7 -0
- package/dist/chunks/program-O2BA5L6Z.js +2755 -0
- package/dist/chunks/program-O2BA5L6Z.js.map +7 -0
- package/dist/chunks/status-I5UAQPBY.js +16 -0
- package/dist/chunks/status-I5UAQPBY.js.map +7 -0
- package/dist/cli.js +66 -0
- package/dist/cli.js.map +7 -0
- package/package.json +65 -0
|
@@ -0,0 +1,3492 @@
|
|
|
1
|
+
import { createRequire as __tabbioCreateRequire } from 'node:module';
|
|
2
|
+
const require = __tabbioCreateRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
Page,
|
|
5
|
+
Spinner,
|
|
6
|
+
Working,
|
|
7
|
+
clipLines,
|
|
8
|
+
displayWidth,
|
|
9
|
+
estimateHeight,
|
|
10
|
+
formatElapsed,
|
|
11
|
+
fromTerminal,
|
|
12
|
+
oneLine,
|
|
13
|
+
padEnd,
|
|
14
|
+
padStart,
|
|
15
|
+
pageMargin,
|
|
16
|
+
safeJson,
|
|
17
|
+
sentenceCase,
|
|
18
|
+
shortId,
|
|
19
|
+
stripAnsi,
|
|
20
|
+
summarizeArgs,
|
|
21
|
+
truncate,
|
|
22
|
+
truncateStart,
|
|
23
|
+
useClockTick,
|
|
24
|
+
useTerminalSize,
|
|
25
|
+
wrapWords
|
|
26
|
+
} from "./chunk-NAWA6NCZ.js";
|
|
27
|
+
import {
|
|
28
|
+
CAPABILITY_CHOICES,
|
|
29
|
+
CAPABILITY_CHOICE_HELP,
|
|
30
|
+
DEFAULT_CAPABILITIES,
|
|
31
|
+
autoFilledProperties,
|
|
32
|
+
capabilityRequestFields,
|
|
33
|
+
chatReducer,
|
|
34
|
+
confirmApproval,
|
|
35
|
+
describeSchema,
|
|
36
|
+
draftSummary,
|
|
37
|
+
exampleInvocation,
|
|
38
|
+
findListWrapper,
|
|
39
|
+
getArtifact,
|
|
40
|
+
getPath,
|
|
41
|
+
getThreadMessages,
|
|
42
|
+
humanCell,
|
|
43
|
+
hyperlink,
|
|
44
|
+
initialChatState,
|
|
45
|
+
isDestructiveTool,
|
|
46
|
+
listModels,
|
|
47
|
+
listThreads,
|
|
48
|
+
loadCapabilityPrefs,
|
|
49
|
+
messageText,
|
|
50
|
+
parseCapabilityMode,
|
|
51
|
+
parseToolInput,
|
|
52
|
+
pickColumns,
|
|
53
|
+
previewUrl,
|
|
54
|
+
previewVariantFor,
|
|
55
|
+
rejectApproval,
|
|
56
|
+
renderScreen,
|
|
57
|
+
saveCapabilityPrefs,
|
|
58
|
+
schemaToFlags,
|
|
59
|
+
streamChat,
|
|
60
|
+
supportsHyperlinks,
|
|
61
|
+
unavailableNotices,
|
|
62
|
+
waitForArtifact
|
|
63
|
+
} from "./chunk-QVR5KIEQ.js";
|
|
64
|
+
import {
|
|
65
|
+
CLI_VERSION,
|
|
66
|
+
getMcpAccessState,
|
|
67
|
+
interruptedError,
|
|
68
|
+
isCliError,
|
|
69
|
+
readCachedCatalog,
|
|
70
|
+
relativeTime,
|
|
71
|
+
toCliError
|
|
72
|
+
} from "./chunk-7LNC3FIV.js";
|
|
73
|
+
import "./chunk-NK5RPNSV.js";
|
|
74
|
+
import {
|
|
75
|
+
theme
|
|
76
|
+
} from "./chunk-VHHZFMIF.js";
|
|
77
|
+
|
|
78
|
+
// src/ui/entry.tsx
|
|
79
|
+
import { Box as Box25, Static as Static2, Text as Text25 } from "ink";
|
|
80
|
+
import { useEffect as useEffect9 } from "react";
|
|
81
|
+
|
|
82
|
+
// src/ui/components/ApprovalCard.tsx
|
|
83
|
+
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
84
|
+
import { useEffect, useState } from "react";
|
|
85
|
+
|
|
86
|
+
// src/ui/components/KeyHints.tsx
|
|
87
|
+
import { Text } from "ink";
|
|
88
|
+
import { memo } from "react";
|
|
89
|
+
import { jsx } from "react/jsx-runtime";
|
|
90
|
+
function formatHints(hints) {
|
|
91
|
+
const sep = theme.dim(` ${theme.symbols.middot} `);
|
|
92
|
+
return hints.map((h) => `${theme.accent(h.key)} ${theme.dim(h.label)}`).join(sep);
|
|
93
|
+
}
|
|
94
|
+
function fitHints(hints, width) {
|
|
95
|
+
const out = [...hints];
|
|
96
|
+
while (out.length > 1 && displayWidth(formatHints(out)) > width) out.pop();
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
var KeyHints = memo(function KeyHints2({ hints, width }) {
|
|
100
|
+
return /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: formatHints(fitHints(hints, width)) });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// src/ui/components/KeyValue.tsx
|
|
104
|
+
import { Box, Text as Text2 } from "ink";
|
|
105
|
+
import { memo as memo2 } from "react";
|
|
106
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
107
|
+
var KeyValue = memo2(function KeyValue2({ rows, width, wrap = false, maxLabelWidth = 24 }) {
|
|
108
|
+
const labelWidth = Math.min(maxLabelWidth, Math.max(0, ...rows.map((r) => displayWidth(r.label))));
|
|
109
|
+
const valueWidth = Math.max(8, width - labelWidth - 2);
|
|
110
|
+
return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", width, children: rows.map((row, i) => {
|
|
111
|
+
const note = row.note ? ` ${row.note}` : "";
|
|
112
|
+
const value = wrap ? row.value : truncate(row.value.replace(/\n/g, " "), Math.max(4, valueWidth - displayWidth(note)));
|
|
113
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
114
|
+
/* @__PURE__ */ jsxs(Text2, { children: [
|
|
115
|
+
theme.dim(padEnd(truncate(row.label, labelWidth), labelWidth)),
|
|
116
|
+
" "
|
|
117
|
+
] }),
|
|
118
|
+
/* @__PURE__ */ jsx2(Box, { width: valueWidth, children: /* @__PURE__ */ jsxs(Text2, { wrap: wrap ? "wrap" : "truncate-end", children: [
|
|
119
|
+
value,
|
|
120
|
+
note ? theme.dim(note) : ""
|
|
121
|
+
] }) })
|
|
122
|
+
] }, `${row.label}-${i}`);
|
|
123
|
+
}) });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// src/ui/components/Rail.tsx
|
|
127
|
+
import { Box as Box2 } from "ink";
|
|
128
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
129
|
+
function railColor(tone) {
|
|
130
|
+
switch (tone) {
|
|
131
|
+
case "accent":
|
|
132
|
+
return theme.accentInk;
|
|
133
|
+
case "attention":
|
|
134
|
+
return "yellow";
|
|
135
|
+
case "danger":
|
|
136
|
+
return "red";
|
|
137
|
+
case "success":
|
|
138
|
+
return "green";
|
|
139
|
+
default:
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function Rail({ tone = "accent", children, width }) {
|
|
144
|
+
return /* @__PURE__ */ jsx3(
|
|
145
|
+
Box2,
|
|
146
|
+
{
|
|
147
|
+
flexDirection: "column",
|
|
148
|
+
borderStyle: theme.railStyle,
|
|
149
|
+
borderTop: false,
|
|
150
|
+
borderRight: false,
|
|
151
|
+
borderBottom: false,
|
|
152
|
+
borderLeftColor: railColor(tone),
|
|
153
|
+
borderLeftDimColor: tone === "muted",
|
|
154
|
+
paddingLeft: 1,
|
|
155
|
+
width,
|
|
156
|
+
children
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/ui/components/approval-model.ts
|
|
162
|
+
var APPROVAL_OPTIONS = [
|
|
163
|
+
{ decision: "approve", label: "Approve", hotkey: "y" },
|
|
164
|
+
{ decision: "reject", label: "Reject", hotkey: "n" },
|
|
165
|
+
{ decision: "leave", label: "Leave pending (decide later in the app)", hotkey: "l" }
|
|
166
|
+
];
|
|
167
|
+
var initialApprovalState = { selected: 0, expanded: false };
|
|
168
|
+
function approvalKeyReducer(state, input, key) {
|
|
169
|
+
if (state.decision) return state;
|
|
170
|
+
const count = APPROVAL_OPTIONS.length;
|
|
171
|
+
if (key.escape) return { ...state, decision: "leave" };
|
|
172
|
+
if (key.ctrl && input === "o") return { ...state, expanded: !state.expanded };
|
|
173
|
+
if (key.upArrow || !key.ctrl && input === "k") return { ...state, selected: (state.selected - 1 + count) % count };
|
|
174
|
+
if (key.downArrow || !key.ctrl && input === "j") return { ...state, selected: (state.selected + 1) % count };
|
|
175
|
+
if (key.return) return { ...state, decision: APPROVAL_OPTIONS[state.selected].decision };
|
|
176
|
+
const byNumber = /^[1-9]$/.test(input) ? APPROVAL_OPTIONS[Number(input) - 1] : void 0;
|
|
177
|
+
const byHotkey = APPROVAL_OPTIONS.find((o) => o.hotkey === input.toLowerCase());
|
|
178
|
+
const chosen = byNumber ?? (key.ctrl || key.meta ? void 0 : byHotkey);
|
|
179
|
+
return chosen ? { ...state, selected: APPROVAL_OPTIONS.indexOf(chosen), decision: chosen.decision } : state;
|
|
180
|
+
}
|
|
181
|
+
function approvalOutcomeText(decision, title, approvalId, durationMs) {
|
|
182
|
+
const time = durationMs !== void 0 ? theme.dim(` ${theme.symbols.middot} ${formatElapsed(durationMs)}`) : "";
|
|
183
|
+
if (decision === "approve") return `${theme.success(theme.symbols.success)} Approved ${title}${time}`;
|
|
184
|
+
if (decision === "reject") return `${theme.error(theme.symbols.error)} Rejected ${title}${time}`;
|
|
185
|
+
return `${theme.attention(theme.symbols.attention)} Left pending ${title} ${theme.dim(shortId(approvalId))}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/ui/components/ApprovalCard.tsx
|
|
189
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
190
|
+
var MAX_ARGS = 8;
|
|
191
|
+
function argRows(args, expanded) {
|
|
192
|
+
const rows = Object.entries(args).filter(([key]) => key !== "userId").map(([key, value]) => ({
|
|
193
|
+
label: key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(),
|
|
194
|
+
value: typeof value === "string" ? oneLine(value) : safeJson(value, 0)
|
|
195
|
+
}));
|
|
196
|
+
if (expanded || rows.length <= MAX_ARGS) return rows;
|
|
197
|
+
return [...rows.slice(0, MAX_ARGS), { label: "", value: theme.dim(`+${rows.length - MAX_ARGS} more (ctrl+o)`) }];
|
|
198
|
+
}
|
|
199
|
+
function ApprovalCard(props) {
|
|
200
|
+
const { approvalId, title, message, args, destructive = false, context, busy, error, isActive, width, onDecision } = props;
|
|
201
|
+
const [state, setState] = useState(initialApprovalState);
|
|
202
|
+
useInput(
|
|
203
|
+
(input, key) => {
|
|
204
|
+
setState((current) => approvalKeyReducer(current, input, key));
|
|
205
|
+
},
|
|
206
|
+
{ isActive: isActive && !busy }
|
|
207
|
+
);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (state.decision) {
|
|
210
|
+
onDecision(state.decision);
|
|
211
|
+
setState((current) => ({ ...current, decision: void 0 }));
|
|
212
|
+
}
|
|
213
|
+
}, [state.decision, onDecision]);
|
|
214
|
+
const inner = Math.max(20, width - 2);
|
|
215
|
+
const optionWidth = Math.max(10, Math.min(inner - 4, 52));
|
|
216
|
+
return /* @__PURE__ */ jsxs2(Rail, { tone: destructive ? "danger" : "attention", width, children: [
|
|
217
|
+
/* @__PURE__ */ jsxs2(Box3, { width: inner, children: [
|
|
218
|
+
/* @__PURE__ */ jsx4(Box3, { flexGrow: 1, children: /* @__PURE__ */ jsxs2(Text3, { children: [
|
|
219
|
+
(destructive ? theme.danger : theme.attention)(theme.symbols.attention),
|
|
220
|
+
" ",
|
|
221
|
+
theme.bold("Approval needed")
|
|
222
|
+
] }) }),
|
|
223
|
+
/* @__PURE__ */ jsx4(Text3, { children: theme.dim(shortId(approvalId)) })
|
|
224
|
+
] }),
|
|
225
|
+
/* @__PURE__ */ jsx4(Text3, { children: " " }),
|
|
226
|
+
/* @__PURE__ */ jsx4(Text3, { wrap: "wrap", children: message ? oneLine(message) : `${theme.bold(title)} wants to run with these inputs` }),
|
|
227
|
+
/* @__PURE__ */ jsx4(Text3, { children: " " }),
|
|
228
|
+
/* @__PURE__ */ jsx4(Box3, { marginLeft: 2, children: /* @__PURE__ */ jsx4(KeyValue, { rows: argRows(args, state.expanded), width: Math.max(16, inner - 2), wrap: state.expanded }) }),
|
|
229
|
+
/* @__PURE__ */ jsx4(Text3, { children: " " }),
|
|
230
|
+
/* @__PURE__ */ jsx4(Text3, { wrap: "truncate-end", children: destructive ? theme.danger("This cannot be undone") : theme.dim(context ?? "Writes to your account") }),
|
|
231
|
+
/* @__PURE__ */ jsx4(Text3, { children: " " }),
|
|
232
|
+
APPROVAL_OPTIONS.map((option, i) => {
|
|
233
|
+
const selected = i === state.selected;
|
|
234
|
+
const pointer = selected ? theme.accent(theme.symbols.pointer) : " ";
|
|
235
|
+
const label = `${i + 1}. ${option.label}`;
|
|
236
|
+
return /* @__PURE__ */ jsxs2(Text3, { wrap: "truncate-end", children: [
|
|
237
|
+
pointer,
|
|
238
|
+
" ",
|
|
239
|
+
selected ? theme.accent(padEnd(label, optionWidth)) : padEnd(label, optionWidth),
|
|
240
|
+
" ",
|
|
241
|
+
theme.dim(option.hotkey)
|
|
242
|
+
] }, option.decision);
|
|
243
|
+
}),
|
|
244
|
+
/* @__PURE__ */ jsx4(Text3, { children: " " }),
|
|
245
|
+
busy ? /* @__PURE__ */ jsx4(Spinner, { label: theme.dim(busy === "approve" ? "Approving\u2026" : busy === "reject" ? "Rejecting\u2026" : "Leaving it pending\u2026") }) : error ? /* @__PURE__ */ jsx4(Text3, { wrap: "wrap", children: theme.error(`${theme.symbols.error} ${error}`) }) : /* @__PURE__ */ jsx4(
|
|
246
|
+
KeyHints,
|
|
247
|
+
{
|
|
248
|
+
width: inner,
|
|
249
|
+
hints: [
|
|
250
|
+
{ key: theme.symbols.upDown, label: "select" },
|
|
251
|
+
{ key: "enter", label: "confirm" },
|
|
252
|
+
{ key: "esc", label: "leave pending" },
|
|
253
|
+
{ key: "ctrl+o", label: "all inputs" }
|
|
254
|
+
]
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
] });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/ui/components/Form.tsx
|
|
261
|
+
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
262
|
+
import { memo as memo3, useEffect as useEffect2, useReducer } from "react";
|
|
263
|
+
|
|
264
|
+
// src/ui/fuzzy.ts
|
|
265
|
+
function rankMatch(query, primary, secondary = "") {
|
|
266
|
+
const q = query.trim().toLowerCase();
|
|
267
|
+
if (!q) return 0;
|
|
268
|
+
const p = primary.toLowerCase();
|
|
269
|
+
if (p === q) return 0;
|
|
270
|
+
if (p.startsWith(q)) return 1;
|
|
271
|
+
if (p.split(/[\s._/-]+/).some((word) => word.startsWith(q))) return 2;
|
|
272
|
+
if (p.includes(q)) return 3;
|
|
273
|
+
if (isSubsequence(q, p)) return 4;
|
|
274
|
+
if (secondary && q.length >= 3 && secondary.toLowerCase().includes(q)) return 5;
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
function isSubsequence(needle, haystack) {
|
|
278
|
+
let i = 0;
|
|
279
|
+
for (const char of haystack) {
|
|
280
|
+
if (char === needle[i]) i += 1;
|
|
281
|
+
if (i === needle.length) return true;
|
|
282
|
+
}
|
|
283
|
+
return needle.length === 0;
|
|
284
|
+
}
|
|
285
|
+
function fuzzyFilter(items, query, fields) {
|
|
286
|
+
if (!query.trim()) return [...items];
|
|
287
|
+
return items.map((item, index) => {
|
|
288
|
+
const [primary, secondary] = fields(item);
|
|
289
|
+
return { item, index, rank: rankMatch(query, primary, secondary) };
|
|
290
|
+
}).filter((entry) => entry.rank !== null).sort((a, b) => a.rank - b.rank || a.index - b.index).map((entry) => entry.item);
|
|
291
|
+
}
|
|
292
|
+
function windowAround(count, cursor, size) {
|
|
293
|
+
if (count <= size) return { start: 0, end: count };
|
|
294
|
+
const half = Math.floor(size / 2);
|
|
295
|
+
const start = Math.max(0, Math.min(count - size, cursor - half));
|
|
296
|
+
return { start, end: start + size };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/ui/components/form-model.ts
|
|
300
|
+
function isRecord(value) {
|
|
301
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
302
|
+
}
|
|
303
|
+
function rawFromValue(spec, value) {
|
|
304
|
+
if (value === void 0) return "";
|
|
305
|
+
if (value === null) return spec.nullable ? "null" : "";
|
|
306
|
+
if (spec.kind === "array" && Array.isArray(value) && value.every((v) => typeof v !== "object")) return value.join(", ");
|
|
307
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
308
|
+
return String(value);
|
|
309
|
+
}
|
|
310
|
+
function createFormState(schema, initial = {}, opts = {}) {
|
|
311
|
+
const skip = new Set(opts.skip ?? []);
|
|
312
|
+
const specs = schemaToFlags(schema).filter((spec) => !skip.has(spec.property));
|
|
313
|
+
const ordered = [...specs.filter((s) => s.required), ...specs.filter((s) => !s.required)];
|
|
314
|
+
const fields = ordered.map((spec) => ({ spec, label: spec.flag }));
|
|
315
|
+
const values = {};
|
|
316
|
+
for (const field of fields) values[field.spec.property] = rawFromValue(field.spec, initial[field.spec.property]);
|
|
317
|
+
const missing = new Set(opts.missingRequired ?? []);
|
|
318
|
+
const firstMissing = fields.findIndex((f) => missing.has(f.spec.property) || f.spec.required && !values[f.spec.property]);
|
|
319
|
+
return {
|
|
320
|
+
schema,
|
|
321
|
+
fields,
|
|
322
|
+
values,
|
|
323
|
+
focus: Math.max(0, firstMissing),
|
|
324
|
+
errors: {},
|
|
325
|
+
status: "editing",
|
|
326
|
+
base: isRecord(initial) ? { ...initial } : {}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function validateField(schema, spec, raw) {
|
|
330
|
+
if (raw.trim() === "") return spec.required ? { error: "Required" } : {};
|
|
331
|
+
try {
|
|
332
|
+
const { input } = parseToolInput(schema, { [spec.attr]: raw });
|
|
333
|
+
return { value: input[spec.property] };
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (isCliError(error)) return { error: error.hint ? `${error.message}. ${error.hint}` : error.message };
|
|
336
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function withError(state, property, error) {
|
|
340
|
+
const errors = { ...state.errors };
|
|
341
|
+
if (error) errors[property] = error;
|
|
342
|
+
else delete errors[property];
|
|
343
|
+
return { ...state, errors };
|
|
344
|
+
}
|
|
345
|
+
function focused(state) {
|
|
346
|
+
return state.fields[state.focus];
|
|
347
|
+
}
|
|
348
|
+
function setValue(state, value) {
|
|
349
|
+
const field = focused(state);
|
|
350
|
+
if (!field) return state;
|
|
351
|
+
return withError({ ...state, values: { ...state.values, [field.spec.property]: value } }, field.spec.property, void 0);
|
|
352
|
+
}
|
|
353
|
+
function choices(spec) {
|
|
354
|
+
if (spec.kind === "boolean") return spec.required ? ["true", "false"] : ["", "true", "false"];
|
|
355
|
+
const values = spec.enumValues ?? [];
|
|
356
|
+
return spec.required ? values : ["", ...values];
|
|
357
|
+
}
|
|
358
|
+
function submit(state) {
|
|
359
|
+
let next = { ...state, errors: {} };
|
|
360
|
+
const result = { ...state.base };
|
|
361
|
+
for (const field of state.fields) {
|
|
362
|
+
const property = field.spec.property;
|
|
363
|
+
const checked = validateField(state.schema, field.spec, state.values[property] ?? "");
|
|
364
|
+
if (checked.error) next = withError(next, property, checked.error);
|
|
365
|
+
else if (checked.value === void 0) delete result[property];
|
|
366
|
+
else result[property] = checked.value;
|
|
367
|
+
}
|
|
368
|
+
const firstError = state.fields.findIndex((f) => next.errors[f.spec.property]);
|
|
369
|
+
if (firstError >= 0) return { ...next, focus: firstError };
|
|
370
|
+
return { ...next, status: "submitted", result };
|
|
371
|
+
}
|
|
372
|
+
function formReducer(state, action) {
|
|
373
|
+
if (state.status !== "editing") return state;
|
|
374
|
+
const field = focused(state);
|
|
375
|
+
const raw = field ? state.values[field.spec.property] ?? "" : "";
|
|
376
|
+
switch (action.type) {
|
|
377
|
+
case "type":
|
|
378
|
+
if (field?.spec.kind === "boolean") {
|
|
379
|
+
const c = action.text.toLowerCase();
|
|
380
|
+
if (c === "y" || c === "t") return setValue(state, "true");
|
|
381
|
+
if (c === "n" || c === "f") return setValue(state, "false");
|
|
382
|
+
if (c === " ") return formReducer(state, { type: "toggle" });
|
|
383
|
+
return state;
|
|
384
|
+
}
|
|
385
|
+
return field ? setValue(state, raw + action.text.replace(/\r\n?|\n/g, " ")) : state;
|
|
386
|
+
case "backspace":
|
|
387
|
+
return field ? setValue(state, field.spec.kind === "boolean" ? "" : [...raw].slice(0, -1).join("")) : state;
|
|
388
|
+
case "clear-field":
|
|
389
|
+
return setValue(state, "");
|
|
390
|
+
case "toggle":
|
|
391
|
+
case "cycle": {
|
|
392
|
+
if (!field || field.spec.kind !== "boolean" && field.spec.kind !== "enum") return state;
|
|
393
|
+
const options = choices(field.spec);
|
|
394
|
+
const delta = action.type === "cycle" ? action.delta : 1;
|
|
395
|
+
const index = Math.max(0, options.indexOf(raw));
|
|
396
|
+
return setValue(state, options[(index + delta + options.length) % options.length] ?? "");
|
|
397
|
+
}
|
|
398
|
+
case "next":
|
|
399
|
+
case "prev": {
|
|
400
|
+
if (state.fields.length === 0) return state;
|
|
401
|
+
const checked = field ? validateField(state.schema, field.spec, raw) : {};
|
|
402
|
+
const next = field && raw.trim() ? withError(state, field.spec.property, checked.error) : state;
|
|
403
|
+
const delta = action.type === "next" ? 1 : -1;
|
|
404
|
+
return { ...next, focus: (state.focus + delta + state.fields.length) % state.fields.length };
|
|
405
|
+
}
|
|
406
|
+
case "enter":
|
|
407
|
+
if (state.fields.length === 0 || state.focus >= state.fields.length - 1) return submit(state);
|
|
408
|
+
return formReducer(state, { type: "next" });
|
|
409
|
+
case "submit":
|
|
410
|
+
return submit(state);
|
|
411
|
+
case "cancel":
|
|
412
|
+
return { ...state, status: "cancelled" };
|
|
413
|
+
default:
|
|
414
|
+
return state;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function formKeyAction(state, input, key) {
|
|
418
|
+
const kind = focused(state)?.spec.kind;
|
|
419
|
+
if (key.escape) return { type: "cancel" };
|
|
420
|
+
if (key.tab) return key.shift ? { type: "prev" } : { type: "next" };
|
|
421
|
+
if (key.downArrow) return { type: "next" };
|
|
422
|
+
if (key.upArrow) return { type: "prev" };
|
|
423
|
+
if (key.return) return { type: "enter" };
|
|
424
|
+
if (key.ctrl && input === "s") return { type: "submit" };
|
|
425
|
+
if (key.ctrl && input === "u") return { type: "clear-field" };
|
|
426
|
+
if ((kind === "enum" || kind === "boolean") && (key.leftArrow || key.rightArrow)) {
|
|
427
|
+
return { type: "cycle", delta: key.leftArrow ? -1 : 1 };
|
|
428
|
+
}
|
|
429
|
+
if (key.backspace || key.delete) return { type: "backspace" };
|
|
430
|
+
if (key.ctrl || key.meta) return null;
|
|
431
|
+
return input ? { type: "type", text: input } : null;
|
|
432
|
+
}
|
|
433
|
+
function shellQuote(text) {
|
|
434
|
+
return /^[\w@%+=:,./-]+$/.test(text) ? text : `'${text.replace(/'/g, `'\\''`)}'`;
|
|
435
|
+
}
|
|
436
|
+
function equivalentCommand(tool, input, skip = []) {
|
|
437
|
+
const parts = ["tabbio", ...tool.commandPath];
|
|
438
|
+
for (const spec of schemaToFlags(tool.inputSchema)) {
|
|
439
|
+
const value = input[spec.property];
|
|
440
|
+
if (value === void 0 || skip.includes(spec.property)) continue;
|
|
441
|
+
if (spec.kind === "boolean" && typeof value === "boolean") {
|
|
442
|
+
parts.push(value ? `--${spec.flag}` : `--${spec.flag}=false`);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const text = spec.kind === "array" && Array.isArray(value) && value.every((v) => typeof v !== "object") ? value.join(",") : typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
446
|
+
parts.push(`--${spec.flag}`, shellQuote(text));
|
|
447
|
+
}
|
|
448
|
+
return parts.join(" ");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/ui/components/Form.tsx
|
|
452
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
453
|
+
function valueText(field, raw, focused2) {
|
|
454
|
+
const kind = field.spec.kind;
|
|
455
|
+
if (kind === "boolean") {
|
|
456
|
+
const on = raw === "true";
|
|
457
|
+
const unset = raw === "";
|
|
458
|
+
const box = unset ? "[ ]" : on ? "[x]" : "[-]";
|
|
459
|
+
const label = unset ? theme.dim("unset") : on ? "yes" : "no";
|
|
460
|
+
return `${focused2 ? theme.accent(box) : box} ${label}`;
|
|
461
|
+
}
|
|
462
|
+
if (kind === "enum") {
|
|
463
|
+
const text = raw || theme.dim("none");
|
|
464
|
+
return focused2 ? `${theme.accent("\u2039")} ${text} ${theme.accent("\u203A")}` : text;
|
|
465
|
+
}
|
|
466
|
+
return raw;
|
|
467
|
+
}
|
|
468
|
+
var FieldRow = memo3(function FieldRow2(props) {
|
|
469
|
+
const { field, raw, focused: focused2, error, labelWidth, width } = props;
|
|
470
|
+
const marker = field.spec.required ? theme.attention("*") : " ";
|
|
471
|
+
const label = padEnd(truncate(field.label, labelWidth), labelWidth);
|
|
472
|
+
const valueWidth = Math.max(8, Math.floor((width - labelWidth - 6) * 0.6));
|
|
473
|
+
const textLike = field.spec.kind !== "boolean" && field.spec.kind !== "enum";
|
|
474
|
+
const shown = textLike ? truncateStart(raw, valueWidth - 1) : valueText(field, raw, focused2);
|
|
475
|
+
const cursor = focused2 && textLike ? theme.accent(theme.symbols.cursor) : "";
|
|
476
|
+
const descBudget = width - labelWidth - 6 - valueWidth - 2;
|
|
477
|
+
const hint = field.spec.kind === "enum" && field.spec.enumValues?.length ? field.spec.enumValues.join(" | ") : field.spec.kind === "array" ? "comma-separated" : field.spec.kind === "json" ? "JSON" : field.spec.description;
|
|
478
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
|
|
479
|
+
/* @__PURE__ */ jsxs3(Text4, { wrap: "truncate-end", children: [
|
|
480
|
+
focused2 ? theme.accent(theme.symbols.pointer) : " ",
|
|
481
|
+
" ",
|
|
482
|
+
focused2 ? theme.bold(label) : label,
|
|
483
|
+
" ",
|
|
484
|
+
marker,
|
|
485
|
+
" ",
|
|
486
|
+
padEnd(`${shown}${cursor}`, valueWidth),
|
|
487
|
+
descBudget > 8 && hint ? ` ${theme.dim(truncate(hint, descBudget))}` : ""
|
|
488
|
+
] }),
|
|
489
|
+
error ? /* @__PURE__ */ jsxs3(Text4, { wrap: "truncate-end", children: [
|
|
490
|
+
" ",
|
|
491
|
+
theme.error(`${theme.symbols.error} ${error}`)
|
|
492
|
+
] }) : null
|
|
493
|
+
] });
|
|
494
|
+
});
|
|
495
|
+
function Form(props) {
|
|
496
|
+
const { width, height, isActive, submitLabel = "run" } = props;
|
|
497
|
+
const [state, dispatch] = useReducer(formReducer, props.initial);
|
|
498
|
+
useInput2(
|
|
499
|
+
(input, key) => {
|
|
500
|
+
const action = formKeyAction(state, input, key);
|
|
501
|
+
if (action) dispatch(action);
|
|
502
|
+
},
|
|
503
|
+
{ isActive: isActive && state.status === "editing" }
|
|
504
|
+
);
|
|
505
|
+
const { onSubmit, onCancel } = props;
|
|
506
|
+
useEffect2(() => {
|
|
507
|
+
if (state.status === "submitted" && state.result) onSubmit(state.result);
|
|
508
|
+
if (state.status === "cancelled") onCancel();
|
|
509
|
+
}, [state.status, state.result, onSubmit, onCancel]);
|
|
510
|
+
const labelWidth = Math.min(24, Math.max(6, ...state.fields.map((f) => displayWidth(f.label))));
|
|
511
|
+
const errorRows = Object.keys(state.errors).length;
|
|
512
|
+
const rows = Math.max(1, height - errorRows);
|
|
513
|
+
const { start, end } = windowAround(state.fields.length, state.focus, rows);
|
|
514
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", width, children: [
|
|
515
|
+
state.fields.length === 0 ? /* @__PURE__ */ jsx5(Text4, { children: theme.dim(" This tool takes no input.") }) : null,
|
|
516
|
+
start > 0 ? /* @__PURE__ */ jsx5(Text4, { children: theme.dim(` ${theme.symbols.ellipsis} ${start} above`) }) : null,
|
|
517
|
+
state.fields.slice(start, end).map((field, i) => /* @__PURE__ */ jsx5(
|
|
518
|
+
FieldRow,
|
|
519
|
+
{
|
|
520
|
+
field,
|
|
521
|
+
raw: state.values[field.spec.property] ?? "",
|
|
522
|
+
focused: start + i === state.focus,
|
|
523
|
+
error: state.errors[field.spec.property],
|
|
524
|
+
labelWidth,
|
|
525
|
+
width
|
|
526
|
+
},
|
|
527
|
+
field.spec.property
|
|
528
|
+
)),
|
|
529
|
+
end < state.fields.length ? /* @__PURE__ */ jsx5(Text4, { children: theme.dim(` ${theme.symbols.ellipsis} ${state.fields.length - end} more`) }) : null,
|
|
530
|
+
/* @__PURE__ */ jsx5(Text4, { children: " " }),
|
|
531
|
+
/* @__PURE__ */ jsx5(
|
|
532
|
+
KeyHints,
|
|
533
|
+
{
|
|
534
|
+
width,
|
|
535
|
+
hints: [
|
|
536
|
+
{ key: "tab", label: "next" },
|
|
537
|
+
{ key: "shift+tab", label: "back" },
|
|
538
|
+
{ key: "enter", label: state.focus >= state.fields.length - 1 ? submitLabel : "next" },
|
|
539
|
+
{ key: "ctrl+s", label: submitLabel },
|
|
540
|
+
{ key: "esc", label: "cancel" }
|
|
541
|
+
]
|
|
542
|
+
}
|
|
543
|
+
)
|
|
544
|
+
] });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/ui/screens/chat/Chat.tsx
|
|
548
|
+
import { Box as Box15, Static, Text as Text15, useInput as useInput5 } from "ink";
|
|
549
|
+
import { memo as memo14, useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo4, useReducer as useReducer3, useRef as useRef4, useState as useState4 } from "react";
|
|
550
|
+
|
|
551
|
+
// src/ui/components/editor-model.ts
|
|
552
|
+
var emptyEditor = { value: "", cursor: 0, history: [], historyIndex: null, draft: "" };
|
|
553
|
+
function chars(value) {
|
|
554
|
+
return [...value];
|
|
555
|
+
}
|
|
556
|
+
function place(state, next, cursor) {
|
|
557
|
+
return { ...state, value: next.join(""), cursor: Math.max(0, Math.min(next.length, cursor)), historyIndex: null };
|
|
558
|
+
}
|
|
559
|
+
function editorReducer(state, action) {
|
|
560
|
+
const list = chars(state.value);
|
|
561
|
+
switch (action.type) {
|
|
562
|
+
case "insert": {
|
|
563
|
+
const text = action.text.replace(/\r\n?/g, "\n");
|
|
564
|
+
const inserted = chars(text);
|
|
565
|
+
return place(state, [...list.slice(0, state.cursor), ...inserted, ...list.slice(state.cursor)], state.cursor + inserted.length);
|
|
566
|
+
}
|
|
567
|
+
case "newline":
|
|
568
|
+
return editorReducer(state, { type: "insert", text: "\n" });
|
|
569
|
+
case "backslash-newline":
|
|
570
|
+
if (state.cursor === 0 || list[state.cursor - 1] !== "\\") return editorReducer(state, { type: "newline" });
|
|
571
|
+
return place(state, [...list.slice(0, state.cursor - 1), "\n", ...list.slice(state.cursor)], state.cursor);
|
|
572
|
+
case "backspace":
|
|
573
|
+
if (state.cursor === 0) return state;
|
|
574
|
+
return place(state, [...list.slice(0, state.cursor - 1), ...list.slice(state.cursor)], state.cursor - 1);
|
|
575
|
+
case "left":
|
|
576
|
+
return { ...state, cursor: Math.max(0, state.cursor - 1) };
|
|
577
|
+
case "right":
|
|
578
|
+
return { ...state, cursor: Math.min(list.length, state.cursor + 1) };
|
|
579
|
+
case "home":
|
|
580
|
+
return { ...state, cursor: 0 };
|
|
581
|
+
case "end":
|
|
582
|
+
return { ...state, cursor: list.length };
|
|
583
|
+
case "kill-line":
|
|
584
|
+
return place(state, list.slice(state.cursor), 0);
|
|
585
|
+
case "kill-word": {
|
|
586
|
+
let i = state.cursor;
|
|
587
|
+
while (i > 0 && /\s/.test(list[i - 1])) i -= 1;
|
|
588
|
+
while (i > 0 && !/\s/.test(list[i - 1])) i -= 1;
|
|
589
|
+
return place(state, [...list.slice(0, i), ...list.slice(state.cursor)], i);
|
|
590
|
+
}
|
|
591
|
+
case "set":
|
|
592
|
+
return { ...state, value: action.value, cursor: chars(action.value).length, historyIndex: null };
|
|
593
|
+
case "clear":
|
|
594
|
+
return { ...state, value: "", cursor: 0, historyIndex: null, draft: "" };
|
|
595
|
+
case "remember": {
|
|
596
|
+
const value = action.value.trim();
|
|
597
|
+
if (!value || state.history[state.history.length - 1] === value) return state;
|
|
598
|
+
return { ...state, history: [...state.history, value].slice(-100) };
|
|
599
|
+
}
|
|
600
|
+
case "history-prev": {
|
|
601
|
+
if (state.history.length === 0) return state;
|
|
602
|
+
const index = state.historyIndex === null ? state.history.length - 1 : Math.max(0, state.historyIndex - 1);
|
|
603
|
+
const value = state.history[index];
|
|
604
|
+
return { ...state, value, cursor: chars(value).length, historyIndex: index, draft: state.historyIndex === null ? state.value : state.draft };
|
|
605
|
+
}
|
|
606
|
+
case "history-next": {
|
|
607
|
+
if (state.historyIndex === null) return state;
|
|
608
|
+
const index = state.historyIndex + 1;
|
|
609
|
+
if (index >= state.history.length) {
|
|
610
|
+
return { ...state, value: state.draft, cursor: chars(state.draft).length, historyIndex: null };
|
|
611
|
+
}
|
|
612
|
+
const value = state.history[index];
|
|
613
|
+
return { ...state, value, cursor: chars(value).length, historyIndex: index };
|
|
614
|
+
}
|
|
615
|
+
default:
|
|
616
|
+
return state;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function editorKeyAction(state, input, key) {
|
|
620
|
+
if (key.return) {
|
|
621
|
+
const list = chars(state.value);
|
|
622
|
+
if (state.cursor > 0 && list[state.cursor - 1] === "\\") return { type: "backslash-newline" };
|
|
623
|
+
return { type: "submit" };
|
|
624
|
+
}
|
|
625
|
+
if (input === "\n" && !key.ctrl) return { type: "newline" };
|
|
626
|
+
if (key.backspace || key.delete) return { type: "backspace" };
|
|
627
|
+
if (key.leftArrow) return { type: "left" };
|
|
628
|
+
if (key.rightArrow) return { type: "right" };
|
|
629
|
+
if (key.upArrow) return { type: "history-prev" };
|
|
630
|
+
if (key.downArrow) return { type: "history-next" };
|
|
631
|
+
if (key.ctrl) {
|
|
632
|
+
if (input === "a") return { type: "home" };
|
|
633
|
+
if (input === "e") return { type: "end" };
|
|
634
|
+
if (input === "u") return { type: "kill-line" };
|
|
635
|
+
if (input === "w") return { type: "kill-word" };
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
if (key.escape || key.tab || key.meta) return null;
|
|
639
|
+
if (input) return { type: "insert", text: input };
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/ui/components/SelectList.tsx
|
|
644
|
+
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
645
|
+
import { memo as memo4, useMemo, useState as useState2 } from "react";
|
|
646
|
+
|
|
647
|
+
// src/ui/components/select-model.ts
|
|
648
|
+
var initialSelectState = { cursor: 0, query: "", filtering: false };
|
|
649
|
+
function visibleItems(items, query) {
|
|
650
|
+
return fuzzyFilter(items, query, (item) => [item.label, item.description]);
|
|
651
|
+
}
|
|
652
|
+
function selectReducer(state, action, count) {
|
|
653
|
+
switch (action.type) {
|
|
654
|
+
case "move":
|
|
655
|
+
return count === 0 ? { ...state, cursor: 0 } : { ...state, cursor: Math.max(0, Math.min(count - 1, state.cursor + action.delta)) };
|
|
656
|
+
case "start-filter":
|
|
657
|
+
return { ...state, filtering: true };
|
|
658
|
+
case "type":
|
|
659
|
+
return { ...state, query: state.query + action.text, cursor: 0 };
|
|
660
|
+
case "backspace":
|
|
661
|
+
return state.query ? { ...state, query: [...state.query].slice(0, -1).join(""), cursor: 0 } : { ...state, filtering: false };
|
|
662
|
+
case "stop-filter":
|
|
663
|
+
return { ...state, filtering: false, query: "", cursor: 0 };
|
|
664
|
+
default:
|
|
665
|
+
return state;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function selectKeyAction(state, input, key, filterable) {
|
|
669
|
+
if (key.upArrow || key.ctrl && input === "p") return { type: "move", delta: -1 };
|
|
670
|
+
if (key.downArrow || key.ctrl && input === "n") return { type: "move", delta: 1 };
|
|
671
|
+
if (key.pageUp) return { type: "move", delta: -10 };
|
|
672
|
+
if (key.pageDown) return { type: "move", delta: 10 };
|
|
673
|
+
if (key.return) return { type: "select" };
|
|
674
|
+
if (key.escape) return state.filtering ? { type: "stop-filter" } : { type: "cancel" };
|
|
675
|
+
if (state.filtering) {
|
|
676
|
+
if (key.backspace || key.delete) return { type: "backspace" };
|
|
677
|
+
if (input && !key.ctrl && !key.meta && !key.tab) return { type: "type", text: input };
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
if (input === "j") return { type: "move", delta: 1 };
|
|
681
|
+
if (input === "k") return { type: "move", delta: -1 };
|
|
682
|
+
if (input === "q") return { type: "cancel" };
|
|
683
|
+
if (input === "/" && filterable) return { type: "start-filter" };
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/ui/components/SelectList.tsx
|
|
688
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
689
|
+
var Row = memo4(function Row2({ item, selected, width }) {
|
|
690
|
+
const meta = item.meta ? ` ${item.meta}` : "";
|
|
691
|
+
const labelBudget = Math.max(4, width - 2 - displayWidth(meta));
|
|
692
|
+
const label = truncate(item.label, labelBudget);
|
|
693
|
+
const descBudget = labelBudget - displayWidth(label) - 2;
|
|
694
|
+
const desc = item.description && descBudget > 6 ? ` ${truncate(item.description, descBudget)}` : "";
|
|
695
|
+
const fill = " ".repeat(Math.max(0, width - 2 - displayWidth(label) - displayWidth(desc) - displayWidth(meta)));
|
|
696
|
+
return /* @__PURE__ */ jsxs4(Text5, { wrap: "truncate-end", children: [
|
|
697
|
+
selected ? theme.accent(theme.symbols.pointer) : " ",
|
|
698
|
+
" ",
|
|
699
|
+
selected ? theme.accent(label) : label,
|
|
700
|
+
theme.dim(desc),
|
|
701
|
+
fill,
|
|
702
|
+
theme.dim(meta)
|
|
703
|
+
] });
|
|
704
|
+
});
|
|
705
|
+
function SelectList(props) {
|
|
706
|
+
const { items, width, height, isActive, filterable = true, emptyText = "Nothing here", showHints = true } = props;
|
|
707
|
+
const [state, setState] = useState2(initialSelectState);
|
|
708
|
+
const shown = useMemo(() => visibleItems(items, state.query), [items, state.query]);
|
|
709
|
+
const cursor = Math.min(state.cursor, Math.max(0, shown.length - 1));
|
|
710
|
+
useInput3(
|
|
711
|
+
(input, key) => {
|
|
712
|
+
const outcome = selectKeyAction(state, input, key, filterable);
|
|
713
|
+
if (!outcome) return;
|
|
714
|
+
if (outcome.type === "select") {
|
|
715
|
+
const item = shown[cursor];
|
|
716
|
+
if (item) props.onSelect(item);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (outcome.type === "cancel") {
|
|
720
|
+
props.onCancel();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const next = selectReducer(state, outcome, outcome.type === "move" ? shown.length : visibleItems(items, state.query).length);
|
|
724
|
+
setState(next);
|
|
725
|
+
props.onHighlight?.(visibleItems(items, next.query)[next.cursor]);
|
|
726
|
+
},
|
|
727
|
+
{ isActive }
|
|
728
|
+
);
|
|
729
|
+
const rows = Math.max(1, height);
|
|
730
|
+
const { start, end } = windowAround(shown.length, cursor, rows);
|
|
731
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", width, children: [
|
|
732
|
+
state.filtering ? /* @__PURE__ */ jsxs4(Text5, { children: [
|
|
733
|
+
theme.accent("/"),
|
|
734
|
+
" ",
|
|
735
|
+
state.query,
|
|
736
|
+
theme.accent(theme.symbols.cursor)
|
|
737
|
+
] }) : null,
|
|
738
|
+
shown.length === 0 ? /* @__PURE__ */ jsx6(Text5, { children: theme.dim(` ${emptyText}`) }) : null,
|
|
739
|
+
shown.slice(start, end).map((item, i) => /* @__PURE__ */ jsx6(Row, { item, selected: isActive && start + i === cursor, width }, item.id)),
|
|
740
|
+
shown.length > rows ? /* @__PURE__ */ jsx6(Text5, { children: theme.dim(` ${cursor + 1} of ${shown.length}`) }) : null,
|
|
741
|
+
showHints ? /* @__PURE__ */ jsx6(
|
|
742
|
+
KeyHints,
|
|
743
|
+
{
|
|
744
|
+
width,
|
|
745
|
+
hints: [
|
|
746
|
+
{ key: theme.symbols.upDown, label: "move" },
|
|
747
|
+
{ key: "enter", label: "open" },
|
|
748
|
+
...filterable ? [{ key: "/", label: "filter" }] : [],
|
|
749
|
+
{ key: "esc", label: "back" }
|
|
750
|
+
]
|
|
751
|
+
}
|
|
752
|
+
) : null
|
|
753
|
+
] });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/ui/components/StatusBar.tsx
|
|
757
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
758
|
+
import { memo as memo5 } from "react";
|
|
759
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
760
|
+
function fitStatusBar(segments, hints, width) {
|
|
761
|
+
const sep = ` ${theme.symbols.middot} `;
|
|
762
|
+
let segs = [...segments];
|
|
763
|
+
let fitted = [...hints];
|
|
764
|
+
const measure = () => displayWidth(segs.join(sep)) + (fitted.length ? displayWidth(formatHints(fitted)) + 2 : 0);
|
|
765
|
+
while (measure() > width && fitted.length > 1) fitted = fitHints(fitted, displayWidth(formatHints(fitted)) - 1);
|
|
766
|
+
while (measure() > width && segs.length > 1) segs = segs.slice(0, -1);
|
|
767
|
+
if (measure() > width) fitted = [];
|
|
768
|
+
return { left: theme.dim(segs.join(sep)), right: fitted.length ? formatHints(fitted) : "" };
|
|
769
|
+
}
|
|
770
|
+
var StatusBar = memo5(function StatusBar2({ segments, hints = [], width }) {
|
|
771
|
+
const clean = segments.filter((s) => typeof s === "string" && s.length > 0);
|
|
772
|
+
const { left, right } = fitStatusBar(clean, hints, width);
|
|
773
|
+
return /* @__PURE__ */ jsxs5(Box6, { width, children: [
|
|
774
|
+
/* @__PURE__ */ jsx7(Box6, { flexGrow: 1, children: /* @__PURE__ */ jsx7(Text6, { wrap: "truncate-end", children: left }) }),
|
|
775
|
+
right ? /* @__PURE__ */ jsx7(Text6, { wrap: "truncate-end", children: right }) : null
|
|
776
|
+
] });
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// src/ui/components/TextInput.tsx
|
|
780
|
+
import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
|
|
781
|
+
import { memo as memo6 } from "react";
|
|
782
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
783
|
+
function cursorCell(char) {
|
|
784
|
+
if (theme.chalk.level > 0) return theme.inverse(char);
|
|
785
|
+
return `${theme.symbols.cursor}${char === " " ? "" : char}`;
|
|
786
|
+
}
|
|
787
|
+
function renderComposerLine(state, width, placeholder, showCursor) {
|
|
788
|
+
const budget = Math.max(4, width);
|
|
789
|
+
const chars2 = [...state.value];
|
|
790
|
+
if (chars2.length === 0) {
|
|
791
|
+
const hint = theme.dim(truncate(placeholder, Math.max(1, budget - 1)));
|
|
792
|
+
return showCursor ? `${cursorCell(" ")}${hint}` : hint;
|
|
793
|
+
}
|
|
794
|
+
const cells = chars2.map((char) => char === "\n" ? { text: theme.dim(theme.symbols.newline), width: displayWidth(theme.symbols.newline) } : { text: char, width: displayWidth(char) });
|
|
795
|
+
cells.push({ text: " ", width: 1 });
|
|
796
|
+
const cursor = Math.min(state.cursor, chars2.length);
|
|
797
|
+
let start = 0;
|
|
798
|
+
let used = cells.slice(0, cursor + 1).reduce((sum, c) => sum + c.width, 0);
|
|
799
|
+
while (used > budget - 1 && start < cursor) {
|
|
800
|
+
used -= cells[start].width;
|
|
801
|
+
start += 1;
|
|
802
|
+
}
|
|
803
|
+
let end = cursor + 1;
|
|
804
|
+
let total = used;
|
|
805
|
+
while (end < cells.length && total + cells[end].width <= budget - (start > 0 ? 1 : 0)) {
|
|
806
|
+
total += cells[end].width;
|
|
807
|
+
end += 1;
|
|
808
|
+
}
|
|
809
|
+
const out = cells.slice(start, end).map((cell, i) => {
|
|
810
|
+
const index = start + i;
|
|
811
|
+
if (index === cursor && showCursor) return cursorCell(index < chars2.length && chars2[index] !== "\n" ? cell.text : " ");
|
|
812
|
+
return cell.text;
|
|
813
|
+
});
|
|
814
|
+
if (end === cells.length && !showCursor) out.pop();
|
|
815
|
+
return `${start > 0 ? theme.dim(theme.symbols.ellipsis) : ""}${out.join("")}`;
|
|
816
|
+
}
|
|
817
|
+
var TextInput = memo6(function TextInput2(props) {
|
|
818
|
+
const { editor, dispatch, onSubmit, onKey, isActive, width, placeholder = "", boxed = false, prompt } = props;
|
|
819
|
+
useInput4(
|
|
820
|
+
(input, key) => {
|
|
821
|
+
if (onKey?.(input, key)) return;
|
|
822
|
+
const action = editorKeyAction(editor, input, key);
|
|
823
|
+
if (!action) return;
|
|
824
|
+
if (action.type === "submit") onSubmit(editor.value);
|
|
825
|
+
else dispatch(action);
|
|
826
|
+
},
|
|
827
|
+
{ isActive }
|
|
828
|
+
);
|
|
829
|
+
const promptText = prompt ?? theme.symbols.pointer;
|
|
830
|
+
const inner = boxed ? width - 4 : width;
|
|
831
|
+
const line = renderComposerLine(editor, Math.max(4, inner - displayWidth(promptText) - 1), placeholder, isActive);
|
|
832
|
+
const content = /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate-end", children: [
|
|
833
|
+
isActive ? theme.accent(promptText) : theme.dim(promptText),
|
|
834
|
+
" ",
|
|
835
|
+
line
|
|
836
|
+
] });
|
|
837
|
+
if (!boxed) return content;
|
|
838
|
+
return /* @__PURE__ */ jsx8(Box7, { borderStyle: theme.boxStyle, borderDimColor: true, paddingX: 1, width, children: content });
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
// src/ui/screens/Palette.tsx
|
|
842
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
843
|
+
import { memo as memo7 } from "react";
|
|
844
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
845
|
+
function paletteMatches(commands, draft) {
|
|
846
|
+
if (!draft.startsWith("/") || /\s/.test(draft)) return [];
|
|
847
|
+
return fuzzyFilter(commands, draft.slice(1), (c) => [c.name, c.description]);
|
|
848
|
+
}
|
|
849
|
+
function paletteArgMatches(commands, draft) {
|
|
850
|
+
const match = /^\/([a-z][\w-]*) (\S*)$/i.exec(draft);
|
|
851
|
+
if (!match) return [];
|
|
852
|
+
const command = commands.find((c) => c.name === match[1].toLowerCase());
|
|
853
|
+
if (!command?.args?.length) return [];
|
|
854
|
+
const typed = (match[2] ?? "").toLowerCase();
|
|
855
|
+
return command.args.filter((arg) => arg.value.startsWith(typed)).map((arg) => ({
|
|
856
|
+
name: `${command.name} ${arg.value}`,
|
|
857
|
+
usage: `/${command.name} ${arg.value}`,
|
|
858
|
+
description: arg.description,
|
|
859
|
+
completion: `/${command.name} ${arg.value}`
|
|
860
|
+
}));
|
|
861
|
+
}
|
|
862
|
+
function parseSlash(text) {
|
|
863
|
+
const match = /^\/([a-z][\w-]*)\s*([\s\S]*)$/i.exec(text.trim());
|
|
864
|
+
return match ? { name: match[1].toLowerCase(), arg: (match[2] ?? "").trim() } : null;
|
|
865
|
+
}
|
|
866
|
+
var Palette = memo7(function Palette2(props) {
|
|
867
|
+
const { commands, selected, width, maxRows = 6 } = props;
|
|
868
|
+
const nameWidth = Math.min(22, Math.max(8, ...commands.map((c) => displayWidth(c.usage ?? `/${c.name}`)))) + 2;
|
|
869
|
+
const { start, end } = windowAround(commands.length, selected, maxRows);
|
|
870
|
+
if (commands.length === 0) return /* @__PURE__ */ jsx9(Text8, { children: theme.dim(" No commands match") });
|
|
871
|
+
return /* @__PURE__ */ jsx9(Box8, { flexDirection: "column", width, children: commands.slice(start, end).map((command, i) => {
|
|
872
|
+
const active = start + i === selected;
|
|
873
|
+
const name = padEnd(truncate(command.usage ?? `/${command.name}`, nameWidth - 1), nameWidth);
|
|
874
|
+
return /* @__PURE__ */ jsxs7(Text8, { wrap: "truncate-end", children: [
|
|
875
|
+
active ? theme.accent(theme.symbols.pointer) : " ",
|
|
876
|
+
" ",
|
|
877
|
+
active ? theme.accent(name) : name,
|
|
878
|
+
theme.dim(command.description),
|
|
879
|
+
command.hint ? ` ${command.hint}` : ""
|
|
880
|
+
] }, command.name);
|
|
881
|
+
}) });
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// src/ui/screens/chat/commands.ts
|
|
885
|
+
function choices2(key) {
|
|
886
|
+
return CAPABILITY_CHOICES[key].map((value) => ({ value, description: CAPABILITY_CHOICE_HELP[key][value] ?? "" }));
|
|
887
|
+
}
|
|
888
|
+
function chatCommands(opts) {
|
|
889
|
+
return [
|
|
890
|
+
{ name: "new", description: "Start a new chat" },
|
|
891
|
+
{ name: "threads", description: "Resume a recent chat" },
|
|
892
|
+
{
|
|
893
|
+
name: "mode",
|
|
894
|
+
usage: "/mode seeker|employer",
|
|
895
|
+
description: "Switch workspace",
|
|
896
|
+
args: [
|
|
897
|
+
{ value: "seeker", description: "Your own career" },
|
|
898
|
+
{ value: "employer", description: "Hiring for a company" }
|
|
899
|
+
]
|
|
900
|
+
},
|
|
901
|
+
{ name: "model", usage: "/model <id|auto>", description: "Pick the model" },
|
|
902
|
+
{ name: "research", usage: "/research auto|on|off", description: "Web research for the next messages", args: choices2("research") },
|
|
903
|
+
{ name: "image", usage: "/image auto|on|off", description: "Image generation", args: choices2("image") },
|
|
904
|
+
{ name: "result", usage: "/result auto|summary|document|\u2026", description: "What to make: answer, document, slides, page, image", args: choices2("result") },
|
|
905
|
+
{ name: "help", description: "Keys and commands" },
|
|
906
|
+
...opts.hostedByHome ? [{ name: "home", description: "Back to the home screen" }] : [],
|
|
907
|
+
{ name: "exit", description: "Quit tabbio" }
|
|
908
|
+
];
|
|
909
|
+
}
|
|
910
|
+
function parseMode(value) {
|
|
911
|
+
const v = value.trim().toLowerCase();
|
|
912
|
+
return v === "seeker" || v === "employer" ? v : null;
|
|
913
|
+
}
|
|
914
|
+
var CHAT_HELP = [
|
|
915
|
+
"Commands: /new \xB7 /threads \xB7 /mode seeker|employer \xB7 /model <id|auto> \xB7 /help \xB7 /exit",
|
|
916
|
+
" /research auto|on|off \xB7 /image auto|on|off \xB7 /result auto|summary|document|slides|page|image",
|
|
917
|
+
" (saved for this profile; sent with every message in seeker mode)",
|
|
918
|
+
"Keys: enter send (queues while streaming) \xB7 ctrl+j or \\ then enter newline \xB7 esc interrupt",
|
|
919
|
+
" \u2191\u2193 history \xB7 shift+tab switch mode \xB7 ctrl+o expand the last tool \xB7 ctrl+c twice quit"
|
|
920
|
+
].join("\n");
|
|
921
|
+
|
|
922
|
+
// src/ui/screens/chat/Transcript.tsx
|
|
923
|
+
import { Box as Box14, Text as Text14 } from "ink";
|
|
924
|
+
import { memo as memo13 } from "react";
|
|
925
|
+
|
|
926
|
+
// src/ui/components/ArtifactRow.tsx
|
|
927
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
928
|
+
import { memo as memo8 } from "react";
|
|
929
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
930
|
+
function kindGlyph(kind) {
|
|
931
|
+
switch (kind) {
|
|
932
|
+
case "document":
|
|
933
|
+
return theme.symbols.kindDocument;
|
|
934
|
+
case "slides":
|
|
935
|
+
return theme.symbols.kindSlides;
|
|
936
|
+
case "image":
|
|
937
|
+
return theme.symbols.kindImage;
|
|
938
|
+
case "page":
|
|
939
|
+
return theme.symbols.kindPage;
|
|
940
|
+
default:
|
|
941
|
+
return theme.symbols.kindFile;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function kindLabel(kind) {
|
|
945
|
+
return kind === "slides" ? "Slides" : kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
946
|
+
}
|
|
947
|
+
function artifactStatusText(artifact, draft) {
|
|
948
|
+
if (artifact.status === "pending") {
|
|
949
|
+
if (draft && draft.phase !== "end") return draftSummary(draft);
|
|
950
|
+
return artifact.previewAvailable ? "preview ready, rendering files" : "rendering";
|
|
951
|
+
}
|
|
952
|
+
return artifact.status;
|
|
953
|
+
}
|
|
954
|
+
var ArtifactRow = memo8(function ArtifactRow2({ artifact, draft, width, hint = true }) {
|
|
955
|
+
const status = artifactStatusText(artifact, draft);
|
|
956
|
+
const tone = artifact.status === "failed" ? theme.error : artifact.status === "ready" ? theme.success : theme.dim;
|
|
957
|
+
const meta = [kindLabel(artifact.kind).toLowerCase(), status, shortId(artifact.id, 6, 4)];
|
|
958
|
+
if (artifact.versionNumber && artifact.versionNumber > 1) meta.push(`v${artifact.versionNumber}`);
|
|
959
|
+
const openHint = hint && artifact.status !== "failed" ? `tabbio artifacts open ${artifact.id}` : "";
|
|
960
|
+
const titleBudget = Math.max(8, Math.min(48, width - 4 - displayWidth(meta.join(" \xB7 ")) - 6));
|
|
961
|
+
const title = truncate(artifact.title || "Untitled", titleBudget);
|
|
962
|
+
const linked = artifact.status === "ready" && artifact.previewUrl;
|
|
963
|
+
return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", width, children: [
|
|
964
|
+
/* @__PURE__ */ jsxs8(Box9, { width, children: [
|
|
965
|
+
artifact.status === "pending" ? /* @__PURE__ */ jsx10(Spinner, {}) : /* @__PURE__ */ jsx10(Text9, { children: artifact.status === "ready" ? theme.success(theme.symbols.success) : theme.error(theme.symbols.error) }),
|
|
966
|
+
/* @__PURE__ */ jsx10(Box9, { flexShrink: 0, marginLeft: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
|
|
967
|
+
theme.accent(kindGlyph(artifact.kind)),
|
|
968
|
+
" ",
|
|
969
|
+
linked ? hyperlink(theme.bold(title), artifact.previewUrl) : theme.bold(title)
|
|
970
|
+
] }) }),
|
|
971
|
+
/* @__PURE__ */ jsx10(Box9, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsxs8(Text9, { wrap: "truncate-end", children: [
|
|
972
|
+
theme.dim(` ${theme.symbols.middot} `),
|
|
973
|
+
tone(meta.join(` ${theme.symbols.middot} `)),
|
|
974
|
+
openHint ? theme.dim(` ${openHint}`) : ""
|
|
975
|
+
] }) })
|
|
976
|
+
] }),
|
|
977
|
+
artifact.failure?.message ? /* @__PURE__ */ jsx10(Text9, { wrap: "truncate-end", children: theme.dim(` ${theme.symbols.arrowRight} ${artifact.failure.message}`) }) : null
|
|
978
|
+
] });
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// src/ui/components/Markdown.tsx
|
|
982
|
+
import { Box as Box11, Text as Text11 } from "ink";
|
|
983
|
+
import { marked } from "marked";
|
|
984
|
+
import { memo as memo10, useMemo as useMemo3 } from "react";
|
|
985
|
+
|
|
986
|
+
// src/ui/components/markdown-inline.ts
|
|
987
|
+
var ENTITIES = {
|
|
988
|
+
"&": "&",
|
|
989
|
+
"<": "<",
|
|
990
|
+
">": ">",
|
|
991
|
+
""": '"',
|
|
992
|
+
"'": "'",
|
|
993
|
+
"'": "'",
|
|
994
|
+
" ": " "
|
|
995
|
+
};
|
|
996
|
+
function decodeEntities(text) {
|
|
997
|
+
return text.replace(/&(?:amp|lt|gt|quot|apos|nbsp|#39);/g, (m) => ENTITIES[m] ?? m).replace(/&#(\d{1,6});/g, (m, code) => {
|
|
998
|
+
const n = Number(code);
|
|
999
|
+
return n > 0 && n < 1114111 ? String.fromCodePoint(n) : m;
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
function childTokens(token) {
|
|
1003
|
+
const tokens = token.tokens;
|
|
1004
|
+
return Array.isArray(tokens) ? tokens : void 0;
|
|
1005
|
+
}
|
|
1006
|
+
function textOf(token) {
|
|
1007
|
+
const text = token.text;
|
|
1008
|
+
if (typeof text === "string") return text;
|
|
1009
|
+
const raw = token.raw;
|
|
1010
|
+
return typeof raw === "string" ? raw : "";
|
|
1011
|
+
}
|
|
1012
|
+
function renderLink(token) {
|
|
1013
|
+
const label = renderInline(token.tokens, token.text);
|
|
1014
|
+
const href = token.href ?? "";
|
|
1015
|
+
const plain = stripAnsi(label).trim();
|
|
1016
|
+
if (!href || plain === href || `mailto:${plain}` === href) return theme.link(label || href);
|
|
1017
|
+
return `${label} ${theme.dim(`(${href})`)}`;
|
|
1018
|
+
}
|
|
1019
|
+
function renderToken(token) {
|
|
1020
|
+
switch (token.type) {
|
|
1021
|
+
case "text": {
|
|
1022
|
+
const children = childTokens(token);
|
|
1023
|
+
return children ? renderInline(children, textOf(token)) : decodeEntities(textOf(token));
|
|
1024
|
+
}
|
|
1025
|
+
case "escape":
|
|
1026
|
+
return textOf(token);
|
|
1027
|
+
case "strong":
|
|
1028
|
+
return theme.bold(renderInline(childTokens(token), textOf(token)));
|
|
1029
|
+
case "em":
|
|
1030
|
+
return theme.italic(renderInline(childTokens(token), textOf(token)));
|
|
1031
|
+
case "del":
|
|
1032
|
+
return theme.strike(renderInline(childTokens(token), textOf(token)));
|
|
1033
|
+
case "codespan":
|
|
1034
|
+
return theme.code(decodeEntities(textOf(token)));
|
|
1035
|
+
case "br":
|
|
1036
|
+
return "\n";
|
|
1037
|
+
case "link":
|
|
1038
|
+
return renderLink(token);
|
|
1039
|
+
case "image": {
|
|
1040
|
+
const image = token;
|
|
1041
|
+
return `${theme.dim("[image]")} ${image.text || ""}${image.href ? ` ${theme.dim(`(${image.href})`)}` : ""}`.trim();
|
|
1042
|
+
}
|
|
1043
|
+
case "html": {
|
|
1044
|
+
const text = textOf(token);
|
|
1045
|
+
return /^<br\s*\/?>$/i.test(text.trim()) ? "\n" : "";
|
|
1046
|
+
}
|
|
1047
|
+
case "checkbox":
|
|
1048
|
+
return "";
|
|
1049
|
+
default:
|
|
1050
|
+
return decodeEntities(textOf(token));
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
function renderInline(tokens, fallback = "") {
|
|
1054
|
+
if (!tokens || tokens.length === 0) return decodeEntities(fallback);
|
|
1055
|
+
let out = "";
|
|
1056
|
+
for (const token of tokens) {
|
|
1057
|
+
try {
|
|
1058
|
+
out += renderToken(token);
|
|
1059
|
+
} catch {
|
|
1060
|
+
out += textOf(token);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return out;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// src/ui/components/Table.tsx
|
|
1067
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
1068
|
+
import { memo as memo9, useMemo as useMemo2 } from "react";
|
|
1069
|
+
|
|
1070
|
+
// src/ui/components/table-layout.ts
|
|
1071
|
+
var TABLE_GAP = 2;
|
|
1072
|
+
var MIN_CELL = 4;
|
|
1073
|
+
function minWidth(column) {
|
|
1074
|
+
return Math.min(Math.max(MIN_CELL, displayWidth(column.header)), 10);
|
|
1075
|
+
}
|
|
1076
|
+
function layoutTable(columns, rows, width, gap = TABLE_GAP) {
|
|
1077
|
+
let visible = [...columns];
|
|
1078
|
+
const natural = (column) => Math.min(
|
|
1079
|
+
column.maxWidth ?? 48,
|
|
1080
|
+
Math.max(displayWidth(column.header), ...rows.map((row) => displayWidth(row[column.key] ?? "")))
|
|
1081
|
+
);
|
|
1082
|
+
let widths = visible.map(natural);
|
|
1083
|
+
const total = () => widths.reduce((a, b) => a + b, 0) + gap * Math.max(0, widths.length - 1);
|
|
1084
|
+
for (; ; ) {
|
|
1085
|
+
while (total() > width) {
|
|
1086
|
+
let widest = -1;
|
|
1087
|
+
for (let i = 0; i < widths.length; i += 1) {
|
|
1088
|
+
const column = visible[i];
|
|
1089
|
+
if (widths[i] > minWidth(column) && (widest < 0 || widths[i] > widths[widest])) {
|
|
1090
|
+
widest = i;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (widest < 0) break;
|
|
1094
|
+
widths[widest] = widths[widest] - 1;
|
|
1095
|
+
}
|
|
1096
|
+
if (total() <= width || visible.length <= 1) break;
|
|
1097
|
+
visible = visible.slice(0, -1);
|
|
1098
|
+
widths = visible.map(natural);
|
|
1099
|
+
}
|
|
1100
|
+
if (visible.length === 1 && total() > width) widths = [Math.max(1, width)];
|
|
1101
|
+
return { columns: visible, widths };
|
|
1102
|
+
}
|
|
1103
|
+
function formatTableRow(layout, cells, gap = TABLE_GAP) {
|
|
1104
|
+
return layout.columns.map((column, i) => {
|
|
1105
|
+
const w = layout.widths[i];
|
|
1106
|
+
const text = truncate((cells[column.key] ?? "").replace(/\s+/g, " "), w);
|
|
1107
|
+
return column.align === "right" ? padStart(text, w) : padEnd(text, w);
|
|
1108
|
+
}).join(" ".repeat(gap)).trimEnd();
|
|
1109
|
+
}
|
|
1110
|
+
function formatTableHeader(layout, gap = TABLE_GAP) {
|
|
1111
|
+
const cells = {};
|
|
1112
|
+
for (const column of layout.columns) cells[column.key] = column.header;
|
|
1113
|
+
return formatTableRow(layout, cells, gap);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/ui/components/Table.tsx
|
|
1117
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1118
|
+
var Table = memo9(function Table2({ columns, rows, width, offset = 0, limit }) {
|
|
1119
|
+
const layout = useMemo2(() => layoutTable(columns, rows, width), [columns, rows, width]);
|
|
1120
|
+
const shown = rows.slice(offset, limit === void 0 ? void 0 : offset + limit);
|
|
1121
|
+
return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", width, children: [
|
|
1122
|
+
/* @__PURE__ */ jsx11(Text10, { wrap: "truncate-end", children: theme.dim(formatTableHeader(layout)) }),
|
|
1123
|
+
shown.map((row, i) => /* @__PURE__ */ jsx11(Text10, { wrap: "truncate-end", children: formatTableRow(layout, row) }, offset + i))
|
|
1124
|
+
] });
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
// src/ui/components/Markdown.tsx
|
|
1128
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1129
|
+
function lexMarkdown(text) {
|
|
1130
|
+
try {
|
|
1131
|
+
return marked.lexer(text.replace(/\r\n?/g, "\n"), { gfm: true });
|
|
1132
|
+
} catch {
|
|
1133
|
+
return [{ type: "paragraph", raw: text, text, tokens: [{ type: "text", raw: text, text }] }];
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function tokensOf(token) {
|
|
1137
|
+
const tokens = token.tokens;
|
|
1138
|
+
return Array.isArray(tokens) ? tokens : void 0;
|
|
1139
|
+
}
|
|
1140
|
+
function textOf2(token) {
|
|
1141
|
+
const text = token.text;
|
|
1142
|
+
return typeof text === "string" ? text : typeof token.raw === "string" ? token.raw : "";
|
|
1143
|
+
}
|
|
1144
|
+
function renderList(list, ctx, key, trailing = "") {
|
|
1145
|
+
const start = typeof list.start === "number" ? list.start : 1;
|
|
1146
|
+
const markers = list.items.map(
|
|
1147
|
+
(item, i) => list.ordered ? `${start + i}.` : item.task ? item.checked ? "[x]" : "[ ]" : theme.symbols.bullet
|
|
1148
|
+
);
|
|
1149
|
+
const markerWidth = Math.max(...markers.map((m) => stripAnsi(m).length)) + 1;
|
|
1150
|
+
return /* @__PURE__ */ jsx12(Box11, { flexDirection: "column", children: list.items.map((item, i) => /* @__PURE__ */ jsxs10(Box11, { children: [
|
|
1151
|
+
/* @__PURE__ */ jsx12(Box11, { width: markerWidth, flexShrink: 0, children: /* @__PURE__ */ jsx12(Text11, { children: list.ordered ? markers[i] : theme.dim(markers[i]) }) }),
|
|
1152
|
+
/* @__PURE__ */ jsx12(Box11, { flexDirection: "column", width: Math.max(8, ctx.width - markerWidth), children: renderBlocks(
|
|
1153
|
+
item.tokens,
|
|
1154
|
+
{ width: Math.max(8, ctx.width - markerWidth), trailing: i === list.items.length - 1 && trailing ? trailing : void 0 },
|
|
1155
|
+
`${key}-${i}`,
|
|
1156
|
+
false
|
|
1157
|
+
) })
|
|
1158
|
+
] }, `${key}-${i}`)) }, key);
|
|
1159
|
+
}
|
|
1160
|
+
function renderTable(table, ctx, key) {
|
|
1161
|
+
const columns = table.header.map((cell, i) => ({
|
|
1162
|
+
key: String(i),
|
|
1163
|
+
header: stripAnsi(renderInline(cell.tokens, cell.text)),
|
|
1164
|
+
align: table.align[i] === "right" ? "right" : "left"
|
|
1165
|
+
}));
|
|
1166
|
+
const rows = table.rows.map((row) => {
|
|
1167
|
+
const record = {};
|
|
1168
|
+
row.forEach((cell, i) => {
|
|
1169
|
+
record[String(i)] = stripAnsi(renderInline(cell.tokens, cell.text));
|
|
1170
|
+
});
|
|
1171
|
+
return record;
|
|
1172
|
+
});
|
|
1173
|
+
return /* @__PURE__ */ jsx12(Table, { columns, rows, width: ctx.width }, key);
|
|
1174
|
+
}
|
|
1175
|
+
function renderBlock(token, ctx, key, trailing) {
|
|
1176
|
+
switch (token.type) {
|
|
1177
|
+
case "heading":
|
|
1178
|
+
return /* @__PURE__ */ jsxs10(Text11, { bold: true, children: [
|
|
1179
|
+
renderInline(tokensOf(token), textOf2(token)),
|
|
1180
|
+
trailing
|
|
1181
|
+
] }, key);
|
|
1182
|
+
case "paragraph":
|
|
1183
|
+
case "text":
|
|
1184
|
+
return /* @__PURE__ */ jsxs10(Text11, { children: [
|
|
1185
|
+
renderInline(tokensOf(token), textOf2(token)),
|
|
1186
|
+
trailing
|
|
1187
|
+
] }, key);
|
|
1188
|
+
case "list":
|
|
1189
|
+
return renderList(token, ctx, key, trailing);
|
|
1190
|
+
case "code": {
|
|
1191
|
+
const lines = textOf2(token).replace(/\n$/, "").split("\n");
|
|
1192
|
+
return /* @__PURE__ */ jsx12(
|
|
1193
|
+
Box11,
|
|
1194
|
+
{
|
|
1195
|
+
flexDirection: "column",
|
|
1196
|
+
marginLeft: 2,
|
|
1197
|
+
borderStyle: theme.unicode ? "single" : "classic",
|
|
1198
|
+
borderTop: false,
|
|
1199
|
+
borderRight: false,
|
|
1200
|
+
borderBottom: false,
|
|
1201
|
+
borderLeftDimColor: true,
|
|
1202
|
+
paddingLeft: 1,
|
|
1203
|
+
children: lines.map((line, i) => /* @__PURE__ */ jsx12(Text11, { children: line || " " }, i))
|
|
1204
|
+
},
|
|
1205
|
+
key
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
case "blockquote":
|
|
1209
|
+
return /* @__PURE__ */ jsx12(Rail, { tone: "muted", children: renderBlocks(tokensOf(token) ?? [], { width: Math.max(8, ctx.width - 2) }, key, false) }, key);
|
|
1210
|
+
case "hr":
|
|
1211
|
+
return /* @__PURE__ */ jsx12(Text11, { children: theme.dim(theme.symbols.line.repeat(Math.max(3, Math.min(ctx.width, 60)))) }, key);
|
|
1212
|
+
case "table":
|
|
1213
|
+
return renderTable(token, ctx, key);
|
|
1214
|
+
case "html":
|
|
1215
|
+
return /* @__PURE__ */ jsx12(Text11, { children: theme.dim(textOf2(token).trim()) }, key);
|
|
1216
|
+
case "space":
|
|
1217
|
+
case "def":
|
|
1218
|
+
return null;
|
|
1219
|
+
default:
|
|
1220
|
+
return /* @__PURE__ */ jsx12(Text11, { children: textOf2(token) }, key);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
var INLINE_TAIL = /* @__PURE__ */ new Set(["paragraph", "text", "heading", "list"]);
|
|
1224
|
+
function renderBlocks(tokens, ctx, keyPrefix, spaced = true) {
|
|
1225
|
+
const blocks = tokens.filter((t) => t.type !== "space" && t.type !== "def");
|
|
1226
|
+
const out = [];
|
|
1227
|
+
blocks.forEach((token, i) => {
|
|
1228
|
+
const isLast = i === blocks.length - 1;
|
|
1229
|
+
const trailing = isLast && ctx.trailing && INLINE_TAIL.has(token.type) ? ctx.trailing : "";
|
|
1230
|
+
let node;
|
|
1231
|
+
try {
|
|
1232
|
+
node = renderBlock(token, ctx, `${keyPrefix}-${i}`, trailing);
|
|
1233
|
+
} catch {
|
|
1234
|
+
node = /* @__PURE__ */ jsx12(Text11, { children: token.raw }, `${keyPrefix}-${i}`);
|
|
1235
|
+
}
|
|
1236
|
+
if (spaced && i > 0) out.push(/* @__PURE__ */ jsx12(Text11, { children: " " }, `${keyPrefix}-${i}-gap`));
|
|
1237
|
+
out.push(node);
|
|
1238
|
+
if (isLast && ctx.trailing && !trailing) out.push(/* @__PURE__ */ jsx12(Text11, { children: ctx.trailing }, `${keyPrefix}-tail`));
|
|
1239
|
+
});
|
|
1240
|
+
if (blocks.length === 0 && ctx.trailing) out.push(/* @__PURE__ */ jsx12(Text11, { children: ctx.trailing }, `${keyPrefix}-tail`));
|
|
1241
|
+
return out;
|
|
1242
|
+
}
|
|
1243
|
+
var Markdown = memo10(function Markdown2({ text, width, trailing }) {
|
|
1244
|
+
const tokens = useMemo3(() => lexMarkdown(text), [text]);
|
|
1245
|
+
return /* @__PURE__ */ jsx12(Box11, { flexDirection: "column", width, children: renderBlocks(tokens, { width, trailing }, "md") });
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// src/ui/components/SourcesList.tsx
|
|
1249
|
+
import { Box as Box12, Text as Text12 } from "ink";
|
|
1250
|
+
import { memo as memo11 } from "react";
|
|
1251
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1252
|
+
var MAX_SOURCES = 8;
|
|
1253
|
+
function hostOf(source) {
|
|
1254
|
+
if (source.domain) return source.domain;
|
|
1255
|
+
try {
|
|
1256
|
+
return new URL(source.url).host;
|
|
1257
|
+
} catch {
|
|
1258
|
+
return "";
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
var SourcesList = memo11(function SourcesList2(props) {
|
|
1262
|
+
const { sources, width } = props;
|
|
1263
|
+
const links = props.links ?? supportsHyperlinks();
|
|
1264
|
+
const shown = sources.slice(0, MAX_SOURCES);
|
|
1265
|
+
const numberWidth = String(shown.length).length;
|
|
1266
|
+
return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", width, children: [
|
|
1267
|
+
/* @__PURE__ */ jsx13(Text12, { children: theme.dim("Sources") }),
|
|
1268
|
+
shown.map((source, i) => {
|
|
1269
|
+
const host = hostOf(source);
|
|
1270
|
+
const title = truncate(source.title, Math.max(10, Math.min(60, width - numberWidth - displayWidth(host) - 8)));
|
|
1271
|
+
return /* @__PURE__ */ jsxs11(Box12, { width, children: [
|
|
1272
|
+
/* @__PURE__ */ jsx13(Box12, { flexShrink: 0, children: /* @__PURE__ */ jsxs11(Text12, { children: [
|
|
1273
|
+
theme.dim(String(i + 1).padStart(numberWidth)),
|
|
1274
|
+
" ",
|
|
1275
|
+
links ? hyperlink(title, source.url) : title
|
|
1276
|
+
] }) }),
|
|
1277
|
+
/* @__PURE__ */ jsx13(Box12, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsxs11(Text12, { wrap: "truncate-end", children: [
|
|
1278
|
+
host && host !== source.title ? theme.dim(` ${theme.symbols.middot} ${host}`) : "",
|
|
1279
|
+
links ? "" : theme.dim(` ${source.url}`)
|
|
1280
|
+
] }) })
|
|
1281
|
+
] }, `${i}-${source.url}`);
|
|
1282
|
+
}),
|
|
1283
|
+
sources.length > shown.length ? /* @__PURE__ */ jsx13(Text12, { children: theme.dim(`+${sources.length - shown.length} more`) }) : null
|
|
1284
|
+
] });
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
// src/ui/components/ToolCallRow.tsx
|
|
1288
|
+
import { Box as Box13, Text as Text13 } from "ink";
|
|
1289
|
+
import { memo as memo12 } from "react";
|
|
1290
|
+
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1291
|
+
function isRecord2(value) {
|
|
1292
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1293
|
+
}
|
|
1294
|
+
function resultSummary(result) {
|
|
1295
|
+
if (Array.isArray(result)) return `${result.length} found`;
|
|
1296
|
+
if (!isRecord2(result)) return "";
|
|
1297
|
+
if (result.ok === false || result.error === true) return "failed";
|
|
1298
|
+
for (const key of ["total", "totalCount", "count"]) {
|
|
1299
|
+
if (typeof result[key] === "number") return `${result[key]} found`;
|
|
1300
|
+
}
|
|
1301
|
+
const arrays = Object.values(result).filter(Array.isArray);
|
|
1302
|
+
return arrays.length === 1 ? `${arrays[0].length} found` : "";
|
|
1303
|
+
}
|
|
1304
|
+
function glyph(call) {
|
|
1305
|
+
if (call.decision === "approve") return /* @__PURE__ */ jsx14(Text13, { children: theme.success(theme.symbols.success) });
|
|
1306
|
+
if (call.decision === "reject") return /* @__PURE__ */ jsx14(Text13, { children: theme.error(theme.symbols.error) });
|
|
1307
|
+
if (call.left) return /* @__PURE__ */ jsx14(Text13, { children: theme.attention(theme.symbols.attention) });
|
|
1308
|
+
switch (call.stage) {
|
|
1309
|
+
case "running":
|
|
1310
|
+
return /* @__PURE__ */ jsx14(Spinner, {});
|
|
1311
|
+
case "pending_approval":
|
|
1312
|
+
return /* @__PURE__ */ jsx14(Text13, { children: theme.attention(theme.symbols.attention) });
|
|
1313
|
+
case "completed":
|
|
1314
|
+
return /* @__PURE__ */ jsx14(Text13, { children: theme.success(theme.symbols.success) });
|
|
1315
|
+
case "failed":
|
|
1316
|
+
return /* @__PURE__ */ jsx14(Text13, { children: theme.error(theme.symbols.error) });
|
|
1317
|
+
default:
|
|
1318
|
+
return /* @__PURE__ */ jsx14(Text13, { children: theme.dim(theme.symbols.error) });
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
function titleText(call) {
|
|
1322
|
+
if (call.decision === "approve") return `Approved ${call.title}`;
|
|
1323
|
+
if (call.decision === "reject") return `Rejected ${call.title}`;
|
|
1324
|
+
if (call.left) return `Left pending ${call.title}`;
|
|
1325
|
+
if (call.stage === "running") return `${call.title}${theme.symbols.ellipsis}`;
|
|
1326
|
+
if (call.stage === "declined") return `${call.title} ${theme.dim("declined")}`;
|
|
1327
|
+
return call.title;
|
|
1328
|
+
}
|
|
1329
|
+
function formatToolDuration(ms) {
|
|
1330
|
+
if (ms === void 0) return null;
|
|
1331
|
+
return ms < 100 ? "<0.1s" : formatElapsed(ms);
|
|
1332
|
+
}
|
|
1333
|
+
var RunningTime = memo12(function RunningTime2({ since }) {
|
|
1334
|
+
useClockTick(true);
|
|
1335
|
+
const elapsed = Date.now() - since;
|
|
1336
|
+
return elapsed >= 1e3 ? /* @__PURE__ */ jsx14(Text13, { children: theme.dim(formatElapsed(elapsed)) }) : null;
|
|
1337
|
+
});
|
|
1338
|
+
var ToolCallRow = memo12(function ToolCallRow2({ call, width, expanded = false }) {
|
|
1339
|
+
const card = cardOf(call);
|
|
1340
|
+
if (card && !(call.stage === "pending_approval" && !call.decision)) {
|
|
1341
|
+
return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", width, children: [
|
|
1342
|
+
/* @__PURE__ */ jsx14(ArtifactRow, { artifact: card, draft: call.draft, width }),
|
|
1343
|
+
expanded ? /* @__PURE__ */ jsx14(ToolCallDetail, { call, width }) : null
|
|
1344
|
+
] });
|
|
1345
|
+
}
|
|
1346
|
+
const waiting = call.stage === "pending_approval" && !call.decision && !call.left;
|
|
1347
|
+
const summary = waiting ? theme.attention("waiting for approval") : call.left && call.approvalId ? theme.dim(shortId(call.approvalId)) : theme.dim(call.denial ? "unavailable" : call.stage === "failed" ? "failed" : call.research ? sourcesSummary(call) : resultSummary(call.result));
|
|
1348
|
+
const measured = formatToolDuration(call.durationMs);
|
|
1349
|
+
const duration = measured ? theme.dim(measured) : null;
|
|
1350
|
+
const running = call.stage === "running" && call.endedAt === void 0;
|
|
1351
|
+
const rightWidth = displayWidth(summary) + 2 + (duration ? displayWidth(duration) : 6);
|
|
1352
|
+
const args = summarizeArgs(call.args, Math.max(8, width - displayWidth(titleText(call)) - rightWidth - 7));
|
|
1353
|
+
return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", width, children: [
|
|
1354
|
+
/* @__PURE__ */ jsxs12(Box13, { width, children: [
|
|
1355
|
+
glyph(call),
|
|
1356
|
+
/* @__PURE__ */ jsx14(Box13, { flexGrow: 1, flexShrink: 1, marginLeft: 1, children: /* @__PURE__ */ jsxs12(Text13, { wrap: "truncate-end", children: [
|
|
1357
|
+
titleText(call),
|
|
1358
|
+
args ? theme.dim(` ${theme.symbols.middot} ${args}`) : ""
|
|
1359
|
+
] }) }),
|
|
1360
|
+
/* @__PURE__ */ jsxs12(Box13, { flexShrink: 0, marginLeft: 1, children: [
|
|
1361
|
+
summary ? /* @__PURE__ */ jsxs12(Text13, { children: [
|
|
1362
|
+
summary,
|
|
1363
|
+
" "
|
|
1364
|
+
] }) : null,
|
|
1365
|
+
duration ? /* @__PURE__ */ jsx14(Text13, { children: duration }) : running ? /* @__PURE__ */ jsx14(RunningTime, { since: call.startedAt }) : null
|
|
1366
|
+
] })
|
|
1367
|
+
] }),
|
|
1368
|
+
call.denial ? /* @__PURE__ */ jsx14(Text13, { wrap: "truncate-end", children: theme.dim(` ${theme.symbols.arrowRight} ${call.denial.message}${call.denial.reason ? ` (${call.denial.reason})` : ""}`) }) : null,
|
|
1369
|
+
expanded ? /* @__PURE__ */ jsx14(ToolCallDetail, { call, width }) : null
|
|
1370
|
+
] });
|
|
1371
|
+
});
|
|
1372
|
+
function cardOf(call) {
|
|
1373
|
+
if (call.artifact) return call.artifact;
|
|
1374
|
+
if (!call.draft || call.stage !== "running") return null;
|
|
1375
|
+
const title = typeof call.args.title === "string" && call.args.title ? call.args.title : call.title;
|
|
1376
|
+
return { id: call.draft.artifactId, kind: call.draft.kind, title, status: "pending" };
|
|
1377
|
+
}
|
|
1378
|
+
function sourcesSummary(call) {
|
|
1379
|
+
const n = call.sources?.length ?? 0;
|
|
1380
|
+
if (call.stage === "running") return "";
|
|
1381
|
+
return n ? `${n} ${n === 1 ? "source" : "sources"}` : "no sources";
|
|
1382
|
+
}
|
|
1383
|
+
function researchLines(call) {
|
|
1384
|
+
const result = isRecord2(call.result) ? call.result : {};
|
|
1385
|
+
const queries = Array.isArray(result.queries) ? result.queries.filter((q) => typeof q === "string") : [];
|
|
1386
|
+
const lines = queries.map((q) => `query ${q}`);
|
|
1387
|
+
(call.sources ?? []).forEach((source, i) => lines.push(`${i + 1} ${source.title} ${theme.dim(source.url)}`));
|
|
1388
|
+
return lines;
|
|
1389
|
+
}
|
|
1390
|
+
var ToolCallDetail = memo12(function ToolCallDetail2({ call, width }) {
|
|
1391
|
+
if (call.research && call.result !== void 0 && !call.denial) {
|
|
1392
|
+
const { lines, hidden } = clipLines(researchLines(call).join("\n"), 16);
|
|
1393
|
+
return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", marginLeft: 2, width: Math.max(10, width - 2), children: [
|
|
1394
|
+
lines.map((line, i) => /* @__PURE__ */ jsxs12(Text13, { wrap: "truncate-end", children: [
|
|
1395
|
+
theme.dim(theme.symbols.gutter),
|
|
1396
|
+
" ",
|
|
1397
|
+
line
|
|
1398
|
+
] }, i)),
|
|
1399
|
+
hidden > 0 ? /* @__PURE__ */ jsx14(Text13, { children: theme.dim(` +${hidden} lines`) }) : null
|
|
1400
|
+
] });
|
|
1401
|
+
}
|
|
1402
|
+
const blocks = [["args", call.args]];
|
|
1403
|
+
if (call.result !== void 0) blocks.push(["result", call.result]);
|
|
1404
|
+
return /* @__PURE__ */ jsx14(Box13, { flexDirection: "column", marginLeft: 2, width: Math.max(10, width - 2), children: blocks.map(([label, value]) => {
|
|
1405
|
+
const { lines, hidden } = clipLines(safeJson(value), 16);
|
|
1406
|
+
return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", children: [
|
|
1407
|
+
/* @__PURE__ */ jsx14(Text13, { children: theme.dim(label) }),
|
|
1408
|
+
lines.map((line, i) => /* @__PURE__ */ jsxs12(Text13, { wrap: "truncate-end", children: [
|
|
1409
|
+
theme.dim(theme.symbols.gutter),
|
|
1410
|
+
" ",
|
|
1411
|
+
line
|
|
1412
|
+
] }, i)),
|
|
1413
|
+
hidden > 0 ? /* @__PURE__ */ jsx14(Text13, { children: theme.dim(` +${hidden} lines`) }) : null
|
|
1414
|
+
] }, label);
|
|
1415
|
+
}) });
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
// src/ui/screens/chat/Transcript.tsx
|
|
1419
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1420
|
+
var ASSISTANT_INDENT = 2;
|
|
1421
|
+
var USER_MAX_LINES = 6;
|
|
1422
|
+
var WAITING_AFTER_MS = 1e4;
|
|
1423
|
+
function noticeGlyph(tone) {
|
|
1424
|
+
if (tone === "error") return theme.error(theme.symbols.error);
|
|
1425
|
+
if (tone === "success") return theme.success(theme.symbols.success);
|
|
1426
|
+
if (tone === "attention") return theme.attention(theme.symbols.attention);
|
|
1427
|
+
return theme.dim(theme.symbols.dot);
|
|
1428
|
+
}
|
|
1429
|
+
function UserTurn({ text, width }) {
|
|
1430
|
+
const lines = text.split("\n");
|
|
1431
|
+
const shown = lines.slice(0, USER_MAX_LINES);
|
|
1432
|
+
return /* @__PURE__ */ jsx15(Box14, { marginTop: 1, children: /* @__PURE__ */ jsxs13(Rail, { tone: "accent", width, children: [
|
|
1433
|
+
shown.map((line, i) => /* @__PURE__ */ jsx15(Text14, { wrap: "wrap", children: line || " " }, i)),
|
|
1434
|
+
lines.length > USER_MAX_LINES ? /* @__PURE__ */ jsx15(Text14, { children: theme.dim(`+${lines.length - USER_MAX_LINES} lines`) }) : null
|
|
1435
|
+
] }) });
|
|
1436
|
+
}
|
|
1437
|
+
var EntryView = memo13(function EntryView2({ entry, width }) {
|
|
1438
|
+
const inner = Math.max(10, width - ASSISTANT_INDENT);
|
|
1439
|
+
switch (entry.kind) {
|
|
1440
|
+
case "header": {
|
|
1441
|
+
const fill = Math.max(1, width - displayWidth(entry.text) - displayWidth(entry.right));
|
|
1442
|
+
return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", width, children: [
|
|
1443
|
+
/* @__PURE__ */ jsxs13(Text14, { wrap: "truncate-end", children: [
|
|
1444
|
+
entry.text,
|
|
1445
|
+
" ".repeat(fill),
|
|
1446
|
+
theme.dim(entry.right)
|
|
1447
|
+
] }),
|
|
1448
|
+
/* @__PURE__ */ jsx15(Text14, { children: theme.dim(theme.symbols.line.repeat(width)) })
|
|
1449
|
+
] });
|
|
1450
|
+
}
|
|
1451
|
+
case "user":
|
|
1452
|
+
return /* @__PURE__ */ jsx15(UserTurn, { text: entry.text, width });
|
|
1453
|
+
case "assistant":
|
|
1454
|
+
return /* @__PURE__ */ jsx15(Box14, { marginTop: 1, children: /* @__PURE__ */ jsxs13(Text14, { children: [
|
|
1455
|
+
theme.accent(theme.symbols.dot),
|
|
1456
|
+
" ",
|
|
1457
|
+
theme.bold("Tabbio")
|
|
1458
|
+
] }) });
|
|
1459
|
+
case "markdown":
|
|
1460
|
+
return /* @__PURE__ */ jsx15(Box14, { paddingLeft: ASSISTANT_INDENT, marginTop: entry.gap ? 1 : 0, width, children: /* @__PURE__ */ jsx15(Markdown, { text: entry.text, width: inner }) });
|
|
1461
|
+
case "tool":
|
|
1462
|
+
return /* @__PURE__ */ jsx15(Box14, { paddingLeft: ASSISTANT_INDENT, marginTop: entry.gap ? 1 : 0, width, children: /* @__PURE__ */ jsx15(ToolCallRow, { call: entry.call, width: inner }) });
|
|
1463
|
+
case "tool-detail":
|
|
1464
|
+
return /* @__PURE__ */ jsxs13(Box14, { paddingLeft: ASSISTANT_INDENT, flexDirection: "column", width, children: [
|
|
1465
|
+
/* @__PURE__ */ jsx15(Text14, { children: theme.dim(`${theme.symbols.expanded} ${entry.call.title}`) }),
|
|
1466
|
+
/* @__PURE__ */ jsx15(ToolCallDetail, { call: entry.call, width: inner })
|
|
1467
|
+
] });
|
|
1468
|
+
case "artifact":
|
|
1469
|
+
return /* @__PURE__ */ jsx15(Box14, { paddingLeft: ASSISTANT_INDENT, width, children: /* @__PURE__ */ jsx15(ArtifactRow, { artifact: entry.artifact, width: inner }) });
|
|
1470
|
+
case "sources":
|
|
1471
|
+
return /* @__PURE__ */ jsx15(Box14, { paddingLeft: ASSISTANT_INDENT, marginTop: 1, width, children: /* @__PURE__ */ jsx15(SourcesList, { sources: entry.sources, width: inner }) });
|
|
1472
|
+
case "notice":
|
|
1473
|
+
return /* @__PURE__ */ jsxs13(Box14, { paddingLeft: ASSISTANT_INDENT, marginTop: 1, flexDirection: "column", width, children: [
|
|
1474
|
+
/* @__PURE__ */ jsxs13(Text14, { wrap: "wrap", children: [
|
|
1475
|
+
noticeGlyph(entry.tone),
|
|
1476
|
+
" ",
|
|
1477
|
+
entry.tone === "error" ? theme.error(entry.text) : entry.tone === "info" ? theme.dim(entry.text) : entry.text
|
|
1478
|
+
] }),
|
|
1479
|
+
entry.hint ? /* @__PURE__ */ jsx15(Text14, { wrap: "wrap", children: theme.dim(` ${theme.symbols.arrowRight} ${entry.hint}`) }) : null
|
|
1480
|
+
] });
|
|
1481
|
+
default:
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
function clipLiveText(text, width, maxLines) {
|
|
1486
|
+
if (estimateHeight(text, width) <= maxLines) return { text, hidden: 0 };
|
|
1487
|
+
const lines = text.split("\n");
|
|
1488
|
+
const kept = [];
|
|
1489
|
+
let used = 1;
|
|
1490
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
1491
|
+
const h = estimateHeight(lines[i], width);
|
|
1492
|
+
if (used + h > maxLines) break;
|
|
1493
|
+
kept.unshift(lines[i]);
|
|
1494
|
+
used += h;
|
|
1495
|
+
}
|
|
1496
|
+
return { text: kept.join("\n"), hidden: lines.length - kept.length };
|
|
1497
|
+
}
|
|
1498
|
+
function WaitingLine({ turn }) {
|
|
1499
|
+
useClockTick(true);
|
|
1500
|
+
const idleFor = Date.now() - turn.lastEventAt;
|
|
1501
|
+
const empty = turn.items.length === 0;
|
|
1502
|
+
if (!empty && idleFor < WAITING_AFTER_MS) return null;
|
|
1503
|
+
const label = empty && idleFor < WAITING_AFTER_MS ? "Thinking\u2026" : "waiting for Tabbio\u2026";
|
|
1504
|
+
return /* @__PURE__ */ jsx15(Box14, { paddingLeft: ASSISTANT_INDENT, children: /* @__PURE__ */ jsx15(Spinner, { label: theme.dim(`${label} ${formatElapsed(Date.now() - (empty ? turn.startedAt : turn.lastEventAt))}`) }) });
|
|
1505
|
+
}
|
|
1506
|
+
function liveGap(prev, item) {
|
|
1507
|
+
if (item.kind === "tool") return prev === "markdown" || prev === "text";
|
|
1508
|
+
return prev === "tool" || prev === "markdown" || item.committed > 0;
|
|
1509
|
+
}
|
|
1510
|
+
var LiveItemView = memo13(function LiveItemView2(props) {
|
|
1511
|
+
const { item, width, expanded, cursor, maxLines, gap } = props;
|
|
1512
|
+
if (item.kind === "tool") {
|
|
1513
|
+
return /* @__PURE__ */ jsx15(Box14, { marginTop: gap ? 1 : 0, children: /* @__PURE__ */ jsx15(ToolCallRow, { call: item.call, width, expanded }) });
|
|
1514
|
+
}
|
|
1515
|
+
const pending = item.text.slice(item.committed);
|
|
1516
|
+
if (!pending && !cursor) return null;
|
|
1517
|
+
const clipped = clipLiveText(pending, width, maxLines);
|
|
1518
|
+
return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", width, marginTop: gap ? 1 : 0, children: [
|
|
1519
|
+
clipped.hidden > 0 ? /* @__PURE__ */ jsx15(Text14, { children: theme.dim(`${theme.symbols.ellipsis} ${clipped.hidden} more lines above (streaming)`) }) : null,
|
|
1520
|
+
/* @__PURE__ */ jsx15(Markdown, { text: clipped.text, width, trailing: cursor ? theme.accent(theme.symbols.cursor) : void 0 })
|
|
1521
|
+
] });
|
|
1522
|
+
});
|
|
1523
|
+
function LiveTurn(props) {
|
|
1524
|
+
const { turn, width, expandTools, maxLines, streaming } = props;
|
|
1525
|
+
const inner = Math.max(10, width - ASSISTANT_INDENT);
|
|
1526
|
+
const last = turn.items[turn.items.length - 1];
|
|
1527
|
+
const tools = turn.items.filter((i) => i.kind === "tool").length;
|
|
1528
|
+
const textBudget = Math.max(3, maxLines - tools - 1);
|
|
1529
|
+
return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", width, children: [
|
|
1530
|
+
/* @__PURE__ */ jsx15(Box14, { flexDirection: "column", paddingLeft: ASSISTANT_INDENT, width, children: turn.items.map((item, i) => {
|
|
1531
|
+
const before = turn.items[i - 1];
|
|
1532
|
+
const prev = before ? before.kind === "tool" ? "tool" : "text" : turn.lastKind;
|
|
1533
|
+
return /* @__PURE__ */ jsx15(
|
|
1534
|
+
LiveItemView,
|
|
1535
|
+
{
|
|
1536
|
+
item,
|
|
1537
|
+
width: inner,
|
|
1538
|
+
expanded: expandTools,
|
|
1539
|
+
cursor: streaming && item === last && item.kind === "text",
|
|
1540
|
+
maxLines: textBudget,
|
|
1541
|
+
gap: liveGap(prev, item)
|
|
1542
|
+
},
|
|
1543
|
+
item.id
|
|
1544
|
+
);
|
|
1545
|
+
}) }),
|
|
1546
|
+
streaming ? /* @__PURE__ */ jsx15(WaitingLine, { turn }) : null
|
|
1547
|
+
] });
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// src/ui/screens/chat/useChatCommands.ts
|
|
1551
|
+
import { useCallback, useRef } from "react";
|
|
1552
|
+
function useChatCommands(deps) {
|
|
1553
|
+
const { session, services, companyId, hostedByHome, onExit, openThreads } = deps;
|
|
1554
|
+
const { state } = session;
|
|
1555
|
+
const availability = useRef(null);
|
|
1556
|
+
const warnIfUnavailable = useCallback(
|
|
1557
|
+
(capabilities) => {
|
|
1558
|
+
if (!services.loadCapabilities) return;
|
|
1559
|
+
availability.current ??= services.loadCapabilities().catch(() => null);
|
|
1560
|
+
void availability.current.then((answer) => {
|
|
1561
|
+
for (const text of unavailableNotices(capabilities, answer)) session.dispatch({ type: "notice", tone: "info", text });
|
|
1562
|
+
});
|
|
1563
|
+
},
|
|
1564
|
+
[services, session]
|
|
1565
|
+
);
|
|
1566
|
+
const setCapability = useCallback(
|
|
1567
|
+
(key, arg) => {
|
|
1568
|
+
if (!arg) {
|
|
1569
|
+
session.dispatch({ type: "notice", tone: "info", text: `${key} ${state.capabilities[key]} ${theme.symbols.middot} /${key} <mode> to change it` });
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
let value;
|
|
1573
|
+
try {
|
|
1574
|
+
value = parseCapabilityMode(key, arg);
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
session.dispatch({ type: "notice", tone: "error", text: toCliError(error).message.replace(/^--/, "/") });
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
const next = { ...state.capabilities, [key]: value };
|
|
1580
|
+
session.dispatch({ type: "set-capability", key, value });
|
|
1581
|
+
let saved = false;
|
|
1582
|
+
try {
|
|
1583
|
+
services.saveCapabilityPrefs?.(next);
|
|
1584
|
+
saved = Boolean(services.saveCapabilityPrefs);
|
|
1585
|
+
} catch {
|
|
1586
|
+
saved = false;
|
|
1587
|
+
}
|
|
1588
|
+
const scope = state.mode === "employer" ? " (employer chats ignore it)" : "";
|
|
1589
|
+
session.dispatch({
|
|
1590
|
+
type: "notice",
|
|
1591
|
+
tone: "info",
|
|
1592
|
+
text: `${key} ${value}${scope}${saved ? ` ${theme.symbols.middot} saved for profile ${services.profileName}` : ""}`
|
|
1593
|
+
});
|
|
1594
|
+
if (state.mode !== "employer") warnIfUnavailable(next);
|
|
1595
|
+
},
|
|
1596
|
+
[session, services, state.capabilities, state.mode, warnIfUnavailable]
|
|
1597
|
+
);
|
|
1598
|
+
return useCallback(
|
|
1599
|
+
(name, arg) => {
|
|
1600
|
+
switch (name) {
|
|
1601
|
+
case "new":
|
|
1602
|
+
session.dispatch({ type: "new-thread" });
|
|
1603
|
+
session.dispatch({ type: "notice", tone: "info", text: `New chat ${theme.symbols.middot} ${state.mode}` });
|
|
1604
|
+
return;
|
|
1605
|
+
case "threads":
|
|
1606
|
+
openThreads();
|
|
1607
|
+
return;
|
|
1608
|
+
case "mode": {
|
|
1609
|
+
const mode = parseMode(arg);
|
|
1610
|
+
if (!mode) session.dispatch({ type: "notice", tone: "error", text: "Use /mode seeker or /mode employer" });
|
|
1611
|
+
else {
|
|
1612
|
+
session.dispatch({ type: "set-mode", mode });
|
|
1613
|
+
session.dispatch({ type: "notice", tone: "info", text: `Mode ${mode} ${theme.symbols.middot} new chat` });
|
|
1614
|
+
}
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
case "model": {
|
|
1618
|
+
if (!arg) {
|
|
1619
|
+
const current = state.modelId ?? "auto";
|
|
1620
|
+
services.listModels({ mode: state.mode, companyId }).then(({ models }) => {
|
|
1621
|
+
const list = models.map((m) => `${m.id === current ? `${theme.symbols.pointer} ` : " "}${m.id} ${m.displayName}${m.locked ? ` (${m.minimumTier} plan)` : ""}`);
|
|
1622
|
+
session.dispatch({ type: "notice", tone: "info", text: [`Model: ${current} \xB7 /model <id> to switch`, ...list].join("\n") });
|
|
1623
|
+
}).catch(() => session.dispatch({ type: "notice", tone: "info", text: `Model: ${current} ${theme.symbols.middot} /model <id> or /model auto` }));
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const modelId = arg === "auto" ? void 0 : arg;
|
|
1627
|
+
session.dispatch({ type: "set-model", modelId });
|
|
1628
|
+
session.dispatch({ type: "notice", tone: "info", text: `Model ${modelId ?? "auto"} for the next messages` });
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
case "research":
|
|
1632
|
+
case "image":
|
|
1633
|
+
case "result":
|
|
1634
|
+
setCapability(name, arg);
|
|
1635
|
+
return;
|
|
1636
|
+
case "help":
|
|
1637
|
+
session.dispatch({ type: "notice", tone: "info", text: CHAT_HELP });
|
|
1638
|
+
return;
|
|
1639
|
+
case "home":
|
|
1640
|
+
onExit(hostedByHome ? "home" : "exit");
|
|
1641
|
+
return;
|
|
1642
|
+
case "exit":
|
|
1643
|
+
case "quit":
|
|
1644
|
+
onExit("exit");
|
|
1645
|
+
return;
|
|
1646
|
+
default:
|
|
1647
|
+
session.dispatch({ type: "notice", tone: "error", text: `Unknown command /${name}`, hint: "Type / to see the commands." });
|
|
1648
|
+
}
|
|
1649
|
+
},
|
|
1650
|
+
[session, state.mode, state.modelId, openThreads, onExit, hostedByHome, services, companyId, setCapability]
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// src/ui/screens/chat/useChatSession.ts
|
|
1655
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useReducer as useReducer2, useRef as useRef3, useState as useState3 } from "react";
|
|
1656
|
+
|
|
1657
|
+
// src/ui/screens/chat/useArtifactFollowUp.ts
|
|
1658
|
+
import { useEffect as useEffect3, useRef as useRef2 } from "react";
|
|
1659
|
+
var FOLLOW_TIMEOUT_MS = 5 * 6e4;
|
|
1660
|
+
function liveArtifacts(state) {
|
|
1661
|
+
const items = state.turn?.items ?? [];
|
|
1662
|
+
return items.flatMap((item) => item.kind === "tool" && item.call.artifact ? [item.call.artifact] : []);
|
|
1663
|
+
}
|
|
1664
|
+
function useArtifactFollowUp(state, dispatch, services) {
|
|
1665
|
+
const followed = useRef2(/* @__PURE__ */ new Map());
|
|
1666
|
+
const alive = useRef2(true);
|
|
1667
|
+
useEffect3(
|
|
1668
|
+
() => () => {
|
|
1669
|
+
alive.current = false;
|
|
1670
|
+
},
|
|
1671
|
+
[]
|
|
1672
|
+
);
|
|
1673
|
+
const cards = liveArtifacts(state);
|
|
1674
|
+
const key = cards.map((c) => `${c.id}:${c.status}:${c.linkPending ? 1 : 0}:${c.released ? 1 : 0}`).join("|");
|
|
1675
|
+
useEffect3(() => {
|
|
1676
|
+
const send = (action) => {
|
|
1677
|
+
if (alive.current) dispatch(action);
|
|
1678
|
+
};
|
|
1679
|
+
const mintLink = async (card) => {
|
|
1680
|
+
try {
|
|
1681
|
+
const url = services.artifactPreviewUrl ? await services.artifactPreviewUrl(card.id, card.kind) : void 0;
|
|
1682
|
+
send({ type: "artifact-status", artifactId: card.id, ...url ? { previewUrl: url } : {} });
|
|
1683
|
+
} catch {
|
|
1684
|
+
send({ type: "artifact-status", artifactId: card.id });
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
for (const card of cards) {
|
|
1688
|
+
if (card.released) continue;
|
|
1689
|
+
const task = followed.current.get(card.id);
|
|
1690
|
+
if (card.status === "pending" && task !== "poll") {
|
|
1691
|
+
followed.current.set(card.id, "poll");
|
|
1692
|
+
const getArtifact2 = services.getArtifact;
|
|
1693
|
+
if (!getArtifact2) {
|
|
1694
|
+
send({ type: "artifact-status", artifactId: card.id, release: true });
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
void (async () => {
|
|
1698
|
+
try {
|
|
1699
|
+
const { artifact, timedOut } = await waitForArtifact(() => getArtifact2(card.id), { timeoutMs: FOLLOW_TIMEOUT_MS });
|
|
1700
|
+
if (!artifact || timedOut) {
|
|
1701
|
+
send({ type: "artifact-status", artifactId: card.id, release: true });
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
const url = artifact.status === "ready" && services.hyperlinks && services.artifactPreviewUrl ? await services.artifactPreviewUrl(card.id, card.kind).catch(() => void 0) : void 0;
|
|
1705
|
+
send({
|
|
1706
|
+
type: "artifact-status",
|
|
1707
|
+
artifactId: card.id,
|
|
1708
|
+
status: artifact.status,
|
|
1709
|
+
title: artifact.title,
|
|
1710
|
+
...url ? { previewUrl: url } : {},
|
|
1711
|
+
...artifact.error ? { failure: artifact.error } : {}
|
|
1712
|
+
});
|
|
1713
|
+
} catch {
|
|
1714
|
+
send({ type: "artifact-status", artifactId: card.id, release: true });
|
|
1715
|
+
}
|
|
1716
|
+
})();
|
|
1717
|
+
} else if (card.status === "ready" && card.linkPending && !task) {
|
|
1718
|
+
followed.current.set(card.id, "link");
|
|
1719
|
+
void mintLink(card);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}, [key, dispatch, services]);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/ui/screens/chat/useChatSession.ts
|
|
1726
|
+
var FLUSH_INTERVAL_MS = 50;
|
|
1727
|
+
var HISTORY_LIMIT = 20;
|
|
1728
|
+
function useChatSession(opts) {
|
|
1729
|
+
const { services, companyId } = opts;
|
|
1730
|
+
const [state, dispatch] = useReducer2(
|
|
1731
|
+
chatReducer,
|
|
1732
|
+
opts,
|
|
1733
|
+
(o) => initialChatState({
|
|
1734
|
+
mode: o.mode,
|
|
1735
|
+
threadId: o.threadId,
|
|
1736
|
+
modelId: o.modelId,
|
|
1737
|
+
capabilities: o.capabilities ?? o.services.capabilityPrefs ?? DEFAULT_CAPABILITIES,
|
|
1738
|
+
links: Boolean(o.services.hyperlinks)
|
|
1739
|
+
})
|
|
1740
|
+
);
|
|
1741
|
+
const stateRef = useRef3(state);
|
|
1742
|
+
stateRef.current = state;
|
|
1743
|
+
const buffer = useRef3([]);
|
|
1744
|
+
const abortRef = useRef3(null);
|
|
1745
|
+
const mounted = useRef3(true);
|
|
1746
|
+
const [queue, setQueue] = useState3([]);
|
|
1747
|
+
const [deciding, setDeciding] = useState3(null);
|
|
1748
|
+
const [loading, setLoading] = useState3(
|
|
1749
|
+
opts.threadId ? { label: "Loading the thread\u2026", since: Date.now() } : null
|
|
1750
|
+
);
|
|
1751
|
+
const flush = useCallback2(() => {
|
|
1752
|
+
const events = buffer.current.splice(0);
|
|
1753
|
+
for (const event of events) dispatch(event);
|
|
1754
|
+
}, []);
|
|
1755
|
+
useEffect4(() => {
|
|
1756
|
+
if (!state.streaming) return void 0;
|
|
1757
|
+
const id = setInterval(() => {
|
|
1758
|
+
flush();
|
|
1759
|
+
dispatch({ type: "tick" });
|
|
1760
|
+
}, FLUSH_INTERVAL_MS);
|
|
1761
|
+
return () => clearInterval(id);
|
|
1762
|
+
}, [state.streaming, flush]);
|
|
1763
|
+
useEffect4(
|
|
1764
|
+
() => () => {
|
|
1765
|
+
mounted.current = false;
|
|
1766
|
+
abortRef.current?.abort();
|
|
1767
|
+
},
|
|
1768
|
+
[]
|
|
1769
|
+
);
|
|
1770
|
+
const send = useCallback2(
|
|
1771
|
+
(text) => {
|
|
1772
|
+
const message = text.trim();
|
|
1773
|
+
if (!message) return;
|
|
1774
|
+
if (stateRef.current.streaming) {
|
|
1775
|
+
setQueue((q) => [...q, message]);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
dispatch({ type: "submit", text: message, at: Date.now() });
|
|
1779
|
+
const controller = new AbortController();
|
|
1780
|
+
abortRef.current = controller;
|
|
1781
|
+
const { mode, threadId, modelId, capabilities } = stateRef.current;
|
|
1782
|
+
void (async () => {
|
|
1783
|
+
try {
|
|
1784
|
+
const request = { mode, message, threadId, modelId, companyId, ...capabilityRequestFields(mode, capabilities) };
|
|
1785
|
+
const stream = services.streamChat(request, controller.signal);
|
|
1786
|
+
for await (const event of stream) buffer.current.push({ ...event, at: Date.now() });
|
|
1787
|
+
if (!mounted.current) return;
|
|
1788
|
+
flush();
|
|
1789
|
+
dispatch({ type: "tick" });
|
|
1790
|
+
} catch (error) {
|
|
1791
|
+
if (!mounted.current) return;
|
|
1792
|
+
flush();
|
|
1793
|
+
const cancelled = controller.signal.aborted || isCliError(error) && error.exitCode === 130;
|
|
1794
|
+
const cli = toCliError(error);
|
|
1795
|
+
dispatch({
|
|
1796
|
+
type: "stream-failed",
|
|
1797
|
+
message: cancelled ? "Stopped" : cli.message,
|
|
1798
|
+
hint: cancelled ? void 0 : cli.hint,
|
|
1799
|
+
cancelled,
|
|
1800
|
+
at: Date.now()
|
|
1801
|
+
});
|
|
1802
|
+
} finally {
|
|
1803
|
+
if (abortRef.current === controller) abortRef.current = null;
|
|
1804
|
+
}
|
|
1805
|
+
})();
|
|
1806
|
+
},
|
|
1807
|
+
[services, companyId, flush]
|
|
1808
|
+
);
|
|
1809
|
+
const interrupt = useCallback2(() => {
|
|
1810
|
+
if (!abortRef.current) return false;
|
|
1811
|
+
abortRef.current.abort();
|
|
1812
|
+
return true;
|
|
1813
|
+
}, []);
|
|
1814
|
+
useEffect4(() => {
|
|
1815
|
+
if (state.streaming || state.approvals.length > 0 || queue.length === 0) return;
|
|
1816
|
+
const [next, ...rest] = queue;
|
|
1817
|
+
setQueue(rest);
|
|
1818
|
+
if (next) send(next);
|
|
1819
|
+
}, [state.streaming, state.approvals.length, queue, send]);
|
|
1820
|
+
const decide = useCallback2(
|
|
1821
|
+
async (approval, decision) => {
|
|
1822
|
+
if (decision === "leave") {
|
|
1823
|
+
dispatch({ type: "approval-decided", approvalId: approval.approvalId, decision, at: Date.now() });
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
setDeciding(decision);
|
|
1827
|
+
try {
|
|
1828
|
+
const data = await services.decideApproval(approval.approvalId, decision, { mode: stateRef.current.mode, companyId });
|
|
1829
|
+
if (!mounted.current) return;
|
|
1830
|
+
dispatch({ type: "approval-decided", approvalId: approval.approvalId, decision, text: data.assistant?.text, at: Date.now() });
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
if (!mounted.current) return;
|
|
1833
|
+
const cli = toCliError(error);
|
|
1834
|
+
dispatch({
|
|
1835
|
+
type: "approval-decided",
|
|
1836
|
+
approvalId: approval.approvalId,
|
|
1837
|
+
decision,
|
|
1838
|
+
error: cli.hint ? `${cli.message}. ${cli.hint}` : cli.message,
|
|
1839
|
+
at: Date.now()
|
|
1840
|
+
});
|
|
1841
|
+
} finally {
|
|
1842
|
+
if (mounted.current) setDeciding(null);
|
|
1843
|
+
}
|
|
1844
|
+
},
|
|
1845
|
+
[services, companyId]
|
|
1846
|
+
);
|
|
1847
|
+
const resume = useCallback2(
|
|
1848
|
+
async (threadId, title) => {
|
|
1849
|
+
setLoading({ label: "Loading the thread\u2026", since: Date.now() });
|
|
1850
|
+
try {
|
|
1851
|
+
const data = await services.getThreadMessages(threadId, { mode: stateRef.current.mode, companyId });
|
|
1852
|
+
if (!mounted.current) return;
|
|
1853
|
+
const messages = data.messages.map((m) => ({ role: m.role, text: messageText(m.content) })).filter((m) => m.text.trim()).slice(-HISTORY_LIMIT);
|
|
1854
|
+
dispatch({ type: "notice", tone: "info", text: `Resumed ${title ? `\u201C${title}\u201D` : "thread"} \xB7 last ${messages.length} messages` });
|
|
1855
|
+
dispatch({ type: "history", threadId, messages });
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
if (!mounted.current) return;
|
|
1858
|
+
const cli = toCliError(error);
|
|
1859
|
+
dispatch({ type: "notice", tone: "error", text: `Could not load the thread: ${cli.message}`, hint: cli.hint });
|
|
1860
|
+
} finally {
|
|
1861
|
+
if (mounted.current) setLoading(null);
|
|
1862
|
+
}
|
|
1863
|
+
},
|
|
1864
|
+
[services, companyId]
|
|
1865
|
+
);
|
|
1866
|
+
const booted = useRef3(false);
|
|
1867
|
+
useEffect4(() => {
|
|
1868
|
+
if (booted.current) return;
|
|
1869
|
+
booted.current = true;
|
|
1870
|
+
void (async () => {
|
|
1871
|
+
if (opts.threadId) await resume(opts.threadId);
|
|
1872
|
+
if (opts.initialMessage && mounted.current) send(opts.initialMessage);
|
|
1873
|
+
})();
|
|
1874
|
+
}, [opts.threadId, opts.initialMessage, resume, send]);
|
|
1875
|
+
useArtifactFollowUp(state, dispatch, services);
|
|
1876
|
+
return { state, dispatch, send, interrupt, decide, deciding, queue, setQueue, resume, loading };
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// src/ui/screens/chat/Chat.tsx
|
|
1880
|
+
import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1881
|
+
var DOUBLE_PRESS_MS = 2e3;
|
|
1882
|
+
var ChatStatus = memo14(function ChatStatus2(props) {
|
|
1883
|
+
useClockTick(props.since !== null);
|
|
1884
|
+
const elapsed = props.since !== null ? formatElapsed(Date.now() - props.since) : null;
|
|
1885
|
+
const hints = props.approval ? [
|
|
1886
|
+
{ key: "y", label: "approve" },
|
|
1887
|
+
{ key: "n", label: "reject" },
|
|
1888
|
+
{ key: "l", label: "later" }
|
|
1889
|
+
] : props.streaming ? [
|
|
1890
|
+
{ key: "enter", label: "queue" },
|
|
1891
|
+
{ key: "esc", label: "interrupt" },
|
|
1892
|
+
{ key: "/", label: "commands" }
|
|
1893
|
+
] : [
|
|
1894
|
+
{ key: "enter", label: "send" },
|
|
1895
|
+
{ key: "/", label: "commands" },
|
|
1896
|
+
{ key: "ctrl+c", label: "quit" }
|
|
1897
|
+
];
|
|
1898
|
+
return /* @__PURE__ */ jsx16(StatusBar, { width: props.width, segments: [...props.segments, elapsed], hints });
|
|
1899
|
+
});
|
|
1900
|
+
function Chat(props) {
|
|
1901
|
+
const { services, width, height, hostedByHome = false, onExit } = props;
|
|
1902
|
+
const gutter = pageMargin(useTerminalSize().columns);
|
|
1903
|
+
const session = useChatSession(props);
|
|
1904
|
+
const { state } = session;
|
|
1905
|
+
const [editor, dispatchEditor] = useReducer3(editorReducer, emptyEditor);
|
|
1906
|
+
const [paletteIndex, setPaletteIndex] = useState4(0);
|
|
1907
|
+
const [overlay, setOverlay] = useState4(null);
|
|
1908
|
+
const [expandTools, setExpandTools] = useState4(false);
|
|
1909
|
+
const [hint, setHint] = useState4(null);
|
|
1910
|
+
const lastCtrlC = useRef4(0);
|
|
1911
|
+
const lastEsc = useRef4(0);
|
|
1912
|
+
const commands = useMemo4(() => chatCommands({ hostedByHome }), [hostedByHome]);
|
|
1913
|
+
const commandMatches = overlay ? [] : paletteMatches(commands, editor.value);
|
|
1914
|
+
const argMatches = overlay || commandMatches.length ? [] : paletteArgMatches(commands, editor.value);
|
|
1915
|
+
const matches = commandMatches.length ? commandMatches : argMatches;
|
|
1916
|
+
const selected = Math.min(paletteIndex, Math.max(0, matches.length - 1));
|
|
1917
|
+
const approval = !state.streaming ? state.approvals[0] : void 0;
|
|
1918
|
+
const composerActive = !overlay && !approval;
|
|
1919
|
+
const [header] = useState4(() => ({
|
|
1920
|
+
kind: "header",
|
|
1921
|
+
id: "chat-header",
|
|
1922
|
+
text: `${theme.bold("Chat")} ${theme.dim(`${theme.symbols.middot} ${props.mode}${props.threadId ? ` ${theme.symbols.middot} resumed` : ""}`)}`,
|
|
1923
|
+
right: `profile ${services.profileName}`
|
|
1924
|
+
}));
|
|
1925
|
+
const staticItems = useMemo4(() => [header, ...state.entries], [header, state.entries]);
|
|
1926
|
+
useEffect5(() => {
|
|
1927
|
+
if (!hint) return void 0;
|
|
1928
|
+
const timer = setTimeout(() => setHint(null), DOUBLE_PRESS_MS);
|
|
1929
|
+
return () => clearTimeout(timer);
|
|
1930
|
+
}, [hint]);
|
|
1931
|
+
const openThreads = useCallback3(() => {
|
|
1932
|
+
setOverlay({ kind: "threads", since: Date.now() });
|
|
1933
|
+
services.listThreads({ mode: state.mode, companyId: props.companyId }).then((threads) => setOverlay((o) => o ? { ...o, threads } : o)).catch((error) => setOverlay((o) => o ? { ...o, error: toCliError(error).message } : o));
|
|
1934
|
+
}, [services, state.mode, props.companyId]);
|
|
1935
|
+
const runCommand = useChatCommands({ session, services, companyId: props.companyId, hostedByHome, onExit, openThreads });
|
|
1936
|
+
const submit2 = useCallback3(
|
|
1937
|
+
(value) => {
|
|
1938
|
+
const text = value.trim();
|
|
1939
|
+
if (!text) return;
|
|
1940
|
+
const slash = parseSlash(text);
|
|
1941
|
+
if (slash) {
|
|
1942
|
+
const exact = commands.find((c) => c.name === slash.name);
|
|
1943
|
+
const choice = exact ?? commandMatches[selected];
|
|
1944
|
+
const picked = exact?.args ? argMatches[selected]?.completion : void 0;
|
|
1945
|
+
if (choice?.args && !slash.arg && !picked) {
|
|
1946
|
+
dispatchEditor({ type: "set", value: `/${choice.name} ` });
|
|
1947
|
+
setPaletteIndex(0);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
let arg = exact ? slash.arg : "";
|
|
1951
|
+
const known = exact?.args?.some((a) => a.value === arg.toLowerCase());
|
|
1952
|
+
if (exact?.args && !known && picked) arg = parseSlash(picked)?.arg ?? arg;
|
|
1953
|
+
dispatchEditor({ type: "remember", value: text });
|
|
1954
|
+
dispatchEditor({ type: "clear" });
|
|
1955
|
+
setPaletteIndex(0);
|
|
1956
|
+
runCommand(choice?.name ?? slash.name, arg);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
dispatchEditor({ type: "remember", value: text });
|
|
1960
|
+
dispatchEditor({ type: "clear" });
|
|
1961
|
+
session.send(text);
|
|
1962
|
+
},
|
|
1963
|
+
[commandMatches, argMatches, selected, commands, runCommand, session]
|
|
1964
|
+
);
|
|
1965
|
+
const onCtrlC = useCallback3(() => {
|
|
1966
|
+
const now = Date.now();
|
|
1967
|
+
if (now - lastCtrlC.current < DOUBLE_PRESS_MS) {
|
|
1968
|
+
session.interrupt();
|
|
1969
|
+
onExit("exit");
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
lastCtrlC.current = now;
|
|
1973
|
+
if (session.interrupt()) setHint("Stopped. Press ctrl+c again to quit.");
|
|
1974
|
+
else {
|
|
1975
|
+
if (editor.value) dispatchEditor({ type: "clear" });
|
|
1976
|
+
setHint("Press ctrl+c again to quit.");
|
|
1977
|
+
}
|
|
1978
|
+
}, [session, onExit, editor.value]);
|
|
1979
|
+
useInput5((input, key) => {
|
|
1980
|
+
if (key.ctrl && input === "c") onCtrlC();
|
|
1981
|
+
});
|
|
1982
|
+
const onComposerKey = useCallback3(
|
|
1983
|
+
(input, key) => {
|
|
1984
|
+
if (key.ctrl && input === "c") return true;
|
|
1985
|
+
if (matches.length > 0) {
|
|
1986
|
+
if (key.upArrow || key.downArrow) {
|
|
1987
|
+
setPaletteIndex((i) => key.upArrow ? Math.max(0, i - 1) : Math.min(matches.length - 1, i + 1));
|
|
1988
|
+
return true;
|
|
1989
|
+
}
|
|
1990
|
+
if (key.tab) {
|
|
1991
|
+
const choice = matches[selected];
|
|
1992
|
+
if (choice?.completion) dispatchEditor({ type: "set", value: choice.completion });
|
|
1993
|
+
else if (choice) dispatchEditor({ type: "set", value: `/${choice.name}${choice.usage ? " " : ""}` });
|
|
1994
|
+
setPaletteIndex(0);
|
|
1995
|
+
return true;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
if (key.escape) {
|
|
1999
|
+
if (session.interrupt()) return true;
|
|
2000
|
+
const now = Date.now();
|
|
2001
|
+
if (!editor.value) {
|
|
2002
|
+
if (hostedByHome) onExit("home");
|
|
2003
|
+
else setHint("Type /exit or press ctrl+c twice to quit.");
|
|
2004
|
+
} else if (matches.length > 0 || now - lastEsc.current < 600) dispatchEditor({ type: "clear" });
|
|
2005
|
+
lastEsc.current = now;
|
|
2006
|
+
return true;
|
|
2007
|
+
}
|
|
2008
|
+
if (key.tab && key.shift) {
|
|
2009
|
+
if (!state.streaming) runCommand("mode", state.mode === "seeker" ? "employer" : "seeker");
|
|
2010
|
+
return true;
|
|
2011
|
+
}
|
|
2012
|
+
if (key.ctrl && input === "o") {
|
|
2013
|
+
const liveTools = state.turn?.items.some((item) => item.kind === "tool");
|
|
2014
|
+
if (liveTools) setExpandTools((v) => !v);
|
|
2015
|
+
else session.dispatch({ type: "expand-last-tool" });
|
|
2016
|
+
return true;
|
|
2017
|
+
}
|
|
2018
|
+
return false;
|
|
2019
|
+
},
|
|
2020
|
+
[matches, selected, session, editor.value, state.streaming, state.mode, state.turn, runCommand, hostedByHome, onExit]
|
|
2021
|
+
);
|
|
2022
|
+
const cardRows = approval ? 14 + Math.min(9, Object.keys(approval.args).length) : 0;
|
|
2023
|
+
const fixedRows = 4 + 1 + (hint ? 1 : 0) + session.queue.length + (matches.length ? Math.min(6, matches.length) : 0) + (overlay ? 12 : 0) + cardRows + 2;
|
|
2024
|
+
const liveBudget = Math.max(3, height - fixedRows - 1);
|
|
2025
|
+
const placeholder = approval ? "Decide the approval above first" : state.streaming ? `Queue a follow-up${theme.symbols.ellipsis} (esc to interrupt)` : "Ask Tabbio anything, or / for commands";
|
|
2026
|
+
const segments = [
|
|
2027
|
+
`profile ${services.profileName}`,
|
|
2028
|
+
state.mode,
|
|
2029
|
+
state.modelId ?? "auto",
|
|
2030
|
+
...capabilitySegments(state.mode, state.capabilities),
|
|
2031
|
+
state.threadId ? shortId(state.threadId, 6, 3) : "new chat"
|
|
2032
|
+
];
|
|
2033
|
+
return /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", width, children: [
|
|
2034
|
+
/* @__PURE__ */ jsx16(Static, { items: staticItems, children: (item) => /* @__PURE__ */ jsx16(Box15, { paddingLeft: gutter, children: /* @__PURE__ */ jsx16(EntryView, { entry: item, width }) }, item.id) }),
|
|
2035
|
+
state.turn ? /* @__PURE__ */ jsx16(LiveTurn, { turn: state.turn, width, expandTools, maxLines: liveBudget, streaming: state.streaming }) : null,
|
|
2036
|
+
session.loading ? /* @__PURE__ */ jsx16(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx16(Working, { label: session.loading.label, since: session.loading.since }) }) : null,
|
|
2037
|
+
approval ? /* @__PURE__ */ jsx16(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx16(
|
|
2038
|
+
ApprovalCard,
|
|
2039
|
+
{
|
|
2040
|
+
approvalId: approval.approvalId,
|
|
2041
|
+
title: approval.title,
|
|
2042
|
+
args: approval.args,
|
|
2043
|
+
destructive: isDestructiveTool(approval.name),
|
|
2044
|
+
context: `Writes to your account ${theme.symbols.middot} profile ${services.profileName}`,
|
|
2045
|
+
busy: session.deciding,
|
|
2046
|
+
isActive: !session.deciding && !overlay,
|
|
2047
|
+
width,
|
|
2048
|
+
onDecision: (decision) => void session.decide(approval, decision)
|
|
2049
|
+
}
|
|
2050
|
+
) }) : null,
|
|
2051
|
+
overlay ? /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", marginTop: 1, children: [
|
|
2052
|
+
/* @__PURE__ */ jsx16(Text15, { children: theme.bold("Recent chats") }),
|
|
2053
|
+
overlay.error ? /* @__PURE__ */ jsx16(Text15, { children: theme.error(`${theme.symbols.error} ${overlay.error}`) }) : !overlay.threads ? /* @__PURE__ */ jsx16(Working, { label: "Loading chats\u2026", since: overlay.since }) : /* @__PURE__ */ jsx16(
|
|
2054
|
+
SelectList,
|
|
2055
|
+
{
|
|
2056
|
+
items: overlay.threads.map((t) => ({
|
|
2057
|
+
id: t.id,
|
|
2058
|
+
label: t.title?.trim() || "Untitled chat",
|
|
2059
|
+
meta: relativeTime(new Date(t.lastMessageAt ?? t.updatedAt))
|
|
2060
|
+
})),
|
|
2061
|
+
width,
|
|
2062
|
+
height: 8,
|
|
2063
|
+
isActive: true,
|
|
2064
|
+
emptyText: "No chats yet",
|
|
2065
|
+
onSelect: (item) => {
|
|
2066
|
+
setOverlay(null);
|
|
2067
|
+
void session.resume(item.id, item.label);
|
|
2068
|
+
},
|
|
2069
|
+
onCancel: () => setOverlay(null)
|
|
2070
|
+
}
|
|
2071
|
+
),
|
|
2072
|
+
overlay.error || !overlay.threads ? /* @__PURE__ */ jsx16(SelectEscape, { onClose: () => setOverlay(null) }) : null
|
|
2073
|
+
] }) : null,
|
|
2074
|
+
session.queue.map((queued, i) => /* @__PURE__ */ jsx16(Text15, { wrap: "truncate-end", children: theme.dim(`${theme.symbols.arrowRight} queued: ${queued}`) }, `q-${i}`)),
|
|
2075
|
+
matches.length > 0 ? /* @__PURE__ */ jsx16(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx16(Palette, { commands: matches, selected, width }) }) : null,
|
|
2076
|
+
/* @__PURE__ */ jsx16(Box15, { marginTop: matches.length > 0 ? 0 : 1, children: /* @__PURE__ */ jsx16(
|
|
2077
|
+
TextInput,
|
|
2078
|
+
{
|
|
2079
|
+
editor,
|
|
2080
|
+
dispatch: dispatchEditor,
|
|
2081
|
+
onSubmit: submit2,
|
|
2082
|
+
onKey: onComposerKey,
|
|
2083
|
+
isActive: composerActive,
|
|
2084
|
+
width,
|
|
2085
|
+
placeholder,
|
|
2086
|
+
boxed: true
|
|
2087
|
+
}
|
|
2088
|
+
) }),
|
|
2089
|
+
hint ? /* @__PURE__ */ jsx16(Text15, { children: theme.attention(hint) }) : null,
|
|
2090
|
+
/* @__PURE__ */ jsx16(
|
|
2091
|
+
ChatStatus,
|
|
2092
|
+
{
|
|
2093
|
+
width,
|
|
2094
|
+
segments,
|
|
2095
|
+
since: state.streaming && state.turn ? state.turn.startedAt : null,
|
|
2096
|
+
streaming: state.streaming,
|
|
2097
|
+
approval: Boolean(approval)
|
|
2098
|
+
}
|
|
2099
|
+
)
|
|
2100
|
+
] });
|
|
2101
|
+
}
|
|
2102
|
+
function capabilitySegments(mode, capabilities) {
|
|
2103
|
+
if (mode === "employer") return [];
|
|
2104
|
+
return ["research", "image", "result"].filter((key) => capabilities[key] !== DEFAULT_CAPABILITIES[key]).map((key) => `${key} ${capabilities[key]}`);
|
|
2105
|
+
}
|
|
2106
|
+
function SelectEscape({ onClose }) {
|
|
2107
|
+
useInput5((input, key) => {
|
|
2108
|
+
if (key.escape || input === "q") onClose();
|
|
2109
|
+
});
|
|
2110
|
+
return null;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// src/ui/screens/home/Home.tsx
|
|
2114
|
+
import { Box as Box24, Text as Text24, useInput as useInput10 } from "ink";
|
|
2115
|
+
import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo7, useReducer as useReducer4, useRef as useRef7, useState as useState9 } from "react";
|
|
2116
|
+
|
|
2117
|
+
// src/ui/screens/Approvals.tsx
|
|
2118
|
+
import { Box as Box18, Text as Text18, useInput as useInput7 } from "ink";
|
|
2119
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef5, useState as useState6 } from "react";
|
|
2120
|
+
|
|
2121
|
+
// src/ui/screens/ResultPanel.tsx
|
|
2122
|
+
import { Box as Box17, Text as Text17, useInput as useInput6 } from "ink";
|
|
2123
|
+
import { useMemo as useMemo5, useState as useState5 } from "react";
|
|
2124
|
+
|
|
2125
|
+
// src/ui/components/Panel.tsx
|
|
2126
|
+
import { Box as Box16, Text as Text16 } from "ink";
|
|
2127
|
+
import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2128
|
+
var BORDER = {
|
|
2129
|
+
round: { tl: "\u256D", tr: "\u256E", h: "\u2500" },
|
|
2130
|
+
classic: { tl: "+", tr: "+", h: "-" }
|
|
2131
|
+
};
|
|
2132
|
+
function toneColor(tone) {
|
|
2133
|
+
if (tone === "accent") return theme.accentInk;
|
|
2134
|
+
if (tone === "success") return "green";
|
|
2135
|
+
if (tone === "danger") return "red";
|
|
2136
|
+
if (tone === "attention") return "yellow";
|
|
2137
|
+
return void 0;
|
|
2138
|
+
}
|
|
2139
|
+
function paintBorder(tone, text) {
|
|
2140
|
+
if (tone === "accent") return theme.accent(text);
|
|
2141
|
+
if (tone === "success") return theme.success(text);
|
|
2142
|
+
if (tone === "danger") return theme.danger(text);
|
|
2143
|
+
if (tone === "attention") return theme.attention(text);
|
|
2144
|
+
return theme.dim(text);
|
|
2145
|
+
}
|
|
2146
|
+
function panelTopLine(width, title = "", right = "", tone = "muted") {
|
|
2147
|
+
const chars2 = BORDER[theme.boxStyle];
|
|
2148
|
+
const inner = Math.max(0, width - 2);
|
|
2149
|
+
const titleText2 = title ? ` ${title} ` : "";
|
|
2150
|
+
let rightText = right ? ` ${right} ` : "";
|
|
2151
|
+
if (2 + displayWidth(titleText2) + displayWidth(rightText) > inner) rightText = "";
|
|
2152
|
+
const fittedTitle = titleText2 && 2 + displayWidth(titleText2) > inner ? ` ${truncate(title, Math.max(1, inner - 4))} ` : titleText2;
|
|
2153
|
+
const used = 1 + displayWidth(fittedTitle) + displayWidth(rightText) + 1;
|
|
2154
|
+
const fill = chars2.h.repeat(Math.max(0, inner - used));
|
|
2155
|
+
return paintBorder(tone, `${chars2.tl}${chars2.h}`) + fittedTitle + paintBorder(tone, fill) + (rightText ? theme.dim(rightText) : "") + paintBorder(tone, `${chars2.h}${chars2.tr}`);
|
|
2156
|
+
}
|
|
2157
|
+
function Panel({
|
|
2158
|
+
title,
|
|
2159
|
+
right,
|
|
2160
|
+
width,
|
|
2161
|
+
tone = "muted",
|
|
2162
|
+
children
|
|
2163
|
+
}) {
|
|
2164
|
+
const color = toneColor(tone);
|
|
2165
|
+
return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", width, children: [
|
|
2166
|
+
/* @__PURE__ */ jsx17(Text16, { children: panelTopLine(width, title, right, tone) }),
|
|
2167
|
+
/* @__PURE__ */ jsx17(
|
|
2168
|
+
Box16,
|
|
2169
|
+
{
|
|
2170
|
+
borderStyle: theme.boxStyle,
|
|
2171
|
+
borderTop: false,
|
|
2172
|
+
borderColor: color,
|
|
2173
|
+
borderDimColor: tone === "muted",
|
|
2174
|
+
paddingX: 1,
|
|
2175
|
+
flexDirection: "column",
|
|
2176
|
+
width,
|
|
2177
|
+
children
|
|
2178
|
+
}
|
|
2179
|
+
)
|
|
2180
|
+
] });
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// src/ui/result-model.ts
|
|
2184
|
+
function isRecord3(value) {
|
|
2185
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2186
|
+
}
|
|
2187
|
+
function isRowList(value) {
|
|
2188
|
+
return Array.isArray(value) && value.length > 0 && value.every(isRecord3);
|
|
2189
|
+
}
|
|
2190
|
+
function tableBody(rows, summary, now) {
|
|
2191
|
+
const picked = pickColumns(rows);
|
|
2192
|
+
const columns = picked.map((c) => ({ key: c.key, header: c.label, align: c.align }));
|
|
2193
|
+
const cells = rows.map((row) => {
|
|
2194
|
+
const record = {};
|
|
2195
|
+
for (const column of picked) record[column.key] = humanCell(getPath(row, column.key), now);
|
|
2196
|
+
return record;
|
|
2197
|
+
});
|
|
2198
|
+
return { kind: "table", columns, rows: cells, total: rows.length, summary };
|
|
2199
|
+
}
|
|
2200
|
+
function kvValue(value, now) {
|
|
2201
|
+
if (Array.isArray(value) && value.some((v) => typeof v === "object" && v !== null)) {
|
|
2202
|
+
return `${value.length} item${value.length === 1 ? "" : "s"}`;
|
|
2203
|
+
}
|
|
2204
|
+
if (isRecord3(value)) {
|
|
2205
|
+
const json = safeJson(value, 0);
|
|
2206
|
+
return json.length > 100 ? `${json.slice(0, 99)}\u2026` : json;
|
|
2207
|
+
}
|
|
2208
|
+
return humanCell(value, now);
|
|
2209
|
+
}
|
|
2210
|
+
function describeResult(result, now = Date.now()) {
|
|
2211
|
+
if (result === null || result === void 0 || result === "") return { kind: "empty" };
|
|
2212
|
+
if (typeof result === "string") return { kind: "text", text: result };
|
|
2213
|
+
if (typeof result !== "object") return { kind: "text", text: String(result) };
|
|
2214
|
+
if (isRowList(result)) return tableBody(result, [], now);
|
|
2215
|
+
if (Array.isArray(result)) {
|
|
2216
|
+
return result.length === 0 ? { kind: "empty" } : { kind: "text", text: result.map((v) => humanCell(v, now)).join("\n") };
|
|
2217
|
+
}
|
|
2218
|
+
const wrapper = findListWrapper(result);
|
|
2219
|
+
if (wrapper) {
|
|
2220
|
+
const summary = Object.entries(result).filter(([key, value]) => key !== wrapper.key && (typeof value !== "object" || value === null) && value !== void 0).map(([key, value]) => `${key} ${humanCell(value, now)}`);
|
|
2221
|
+
if (wrapper.rows.length === 0) return { kind: "kv", rows: [{ label: wrapper.key, value: "none" }] };
|
|
2222
|
+
return tableBody(wrapper.rows, summary, now);
|
|
2223
|
+
}
|
|
2224
|
+
const rows = Object.entries(result).filter(([, value]) => value !== void 0).map(([label, value]) => ({ label, value: kvValue(value, now) }));
|
|
2225
|
+
return rows.length ? { kind: "kv", rows } : { kind: "empty" };
|
|
2226
|
+
}
|
|
2227
|
+
function bodyLength(body) {
|
|
2228
|
+
switch (body.kind) {
|
|
2229
|
+
case "table":
|
|
2230
|
+
return body.rows.length;
|
|
2231
|
+
case "kv":
|
|
2232
|
+
return body.rows.length;
|
|
2233
|
+
case "text":
|
|
2234
|
+
return body.text.split("\n").length;
|
|
2235
|
+
default:
|
|
2236
|
+
return 1;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// src/ui/screens/ResultPanel.tsx
|
|
2241
|
+
import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2242
|
+
function summaryLine(body) {
|
|
2243
|
+
switch (body.kind) {
|
|
2244
|
+
case "table":
|
|
2245
|
+
return [`${body.total} result${body.total === 1 ? "" : "s"}`, ...body.summary].join(` ${theme.symbols.middot} `);
|
|
2246
|
+
case "kv":
|
|
2247
|
+
return `${body.rows.length} field${body.rows.length === 1 ? "" : "s"}`;
|
|
2248
|
+
case "text":
|
|
2249
|
+
return "Text result";
|
|
2250
|
+
default:
|
|
2251
|
+
return "No output";
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
function ErrorBody({ error }) {
|
|
2255
|
+
return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", children: [
|
|
2256
|
+
error.hint ? /* @__PURE__ */ jsx18(Text17, { wrap: "wrap", children: theme.dim(`${theme.symbols.arrowRight} ${error.hint}`) }) : null,
|
|
2257
|
+
/* @__PURE__ */ jsx18(Text17, { children: theme.dim([error.requestId ? `request id ${error.requestId}` : null, `exit ${error.exitCode}`].filter(Boolean).join(` ${theme.symbols.middot} `)) })
|
|
2258
|
+
] });
|
|
2259
|
+
}
|
|
2260
|
+
function ResultPanel(props) {
|
|
2261
|
+
const { title, result, error, durationMs, requestId, note, width, height, interactive } = props;
|
|
2262
|
+
const body = useMemo5(() => describeResult(result), [result]);
|
|
2263
|
+
const jsonLines = useMemo5(() => safeJson(result ?? null).split("\n"), [result]);
|
|
2264
|
+
const [view, setView] = useState5("human");
|
|
2265
|
+
const [offset, setOffset] = useState5(0);
|
|
2266
|
+
const total = view === "json" ? jsonLines.length : bodyLength(body);
|
|
2267
|
+
const chrome = 3 + (note ? 1 : 0) + 3;
|
|
2268
|
+
const window = interactive ? Math.max(3, height - chrome - (body.kind === "table" ? 1 : 0)) : total;
|
|
2269
|
+
const maxOffset = Math.max(0, total - window);
|
|
2270
|
+
const clamped = Math.min(offset, maxOffset);
|
|
2271
|
+
useInput6(
|
|
2272
|
+
(input, key) => {
|
|
2273
|
+
if (key.escape || input === "q") props.onBack?.();
|
|
2274
|
+
else if (input === "j") {
|
|
2275
|
+
setView((v) => v === "json" ? "human" : "json");
|
|
2276
|
+
setOffset(0);
|
|
2277
|
+
} else if (input === "r") props.onRerun?.();
|
|
2278
|
+
else if (input === "e") props.onEdit?.();
|
|
2279
|
+
else if (key.upArrow) setOffset((o) => Math.max(0, Math.min(o, maxOffset) - 1));
|
|
2280
|
+
else if (key.downArrow) setOffset((o) => Math.min(maxOffset, o + 1));
|
|
2281
|
+
else if (key.pageUp) setOffset((o) => Math.max(0, Math.min(o, maxOffset) - window));
|
|
2282
|
+
else if (key.pageDown) setOffset((o) => Math.min(maxOffset, o + window));
|
|
2283
|
+
else if (input === "g") setOffset(0);
|
|
2284
|
+
else if (input === "G") setOffset(maxOffset);
|
|
2285
|
+
},
|
|
2286
|
+
{ isActive: interactive }
|
|
2287
|
+
);
|
|
2288
|
+
const ok = !error;
|
|
2289
|
+
const heading = `${ok ? theme.success(theme.symbols.success) : theme.error(theme.symbols.error)} ${theme.bold(title)}`;
|
|
2290
|
+
const right = [formatElapsed(durationMs), requestId ? `req ${shortId(requestId, 4, 3)}` : null].filter(Boolean).join(` ${theme.symbols.middot} `);
|
|
2291
|
+
const inner = Math.max(10, width - 4);
|
|
2292
|
+
const hints = [
|
|
2293
|
+
{ key: "j", label: view === "json" ? "table" : "json" },
|
|
2294
|
+
...props.onRerun ? [{ key: "r", label: "re-run" }] : [],
|
|
2295
|
+
...props.onEdit ? [{ key: "e", label: "edit input" }] : [],
|
|
2296
|
+
...total > window ? [{ key: theme.symbols.upDown, label: "scroll" }] : [],
|
|
2297
|
+
{ key: "q", label: "back" }
|
|
2298
|
+
];
|
|
2299
|
+
return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", width, children: [
|
|
2300
|
+
/* @__PURE__ */ jsx18(Panel, { title: heading, right, width, tone: ok ? "muted" : "danger", children: /* @__PURE__ */ jsx18(Text17, { wrap: "truncate-end", children: error ? theme.error(error.message) : theme.dim(summaryLine(body)) }) }),
|
|
2301
|
+
note ? /* @__PURE__ */ jsx18(Text17, { wrap: "truncate-end", children: note }) : null,
|
|
2302
|
+
/* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", paddingLeft: 2, width, children: [
|
|
2303
|
+
error ? /* @__PURE__ */ jsx18(ErrorBody, { error }) : view === "json" ? jsonLines.slice(clamped, clamped + window).map((line, i) => /* @__PURE__ */ jsx18(Text17, { wrap: "truncate-end", children: line }, clamped + i)) : body.kind === "table" ? /* @__PURE__ */ jsx18(Table, { columns: body.columns, rows: body.rows, width: inner, offset: clamped, limit: window }) : body.kind === "kv" ? /* @__PURE__ */ jsx18(KeyValue, { rows: body.rows.slice(clamped, clamped + window), width: inner }) : body.kind === "text" ? interactive ? body.text.split("\n").slice(clamped, clamped + window).map((line, i) => /* @__PURE__ */ jsx18(Text17, { wrap: "truncate-end", children: line }, clamped + i)) : /* @__PURE__ */ jsx18(Markdown, { text: body.text, width: inner }) : /* @__PURE__ */ jsx18(Text17, { children: theme.dim("No output") }),
|
|
2304
|
+
interactive && total > window ? /* @__PURE__ */ jsx18(Text17, { children: theme.dim(`${clamped + 1}\u2013${Math.min(total, clamped + window)} of ${total}`) }) : null
|
|
2305
|
+
] }),
|
|
2306
|
+
interactive ? /* @__PURE__ */ jsx18(Box17, { marginTop: 1, children: /* @__PURE__ */ jsx18(KeyHints, { width, hints }) }) : null
|
|
2307
|
+
] });
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// src/ui/screens/Approvals.tsx
|
|
2311
|
+
import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
2312
|
+
function argsOf(item) {
|
|
2313
|
+
return typeof item.args === "object" && item.args !== null && !Array.isArray(item.args) ? item.args : {};
|
|
2314
|
+
}
|
|
2315
|
+
function Approvals({ services, width, height, onExit }) {
|
|
2316
|
+
const [phase, setPhase] = useState6({ name: "loading", since: Date.now() });
|
|
2317
|
+
const mounted = useRef5(true);
|
|
2318
|
+
useEffect6(
|
|
2319
|
+
() => () => {
|
|
2320
|
+
mounted.current = false;
|
|
2321
|
+
},
|
|
2322
|
+
[]
|
|
2323
|
+
);
|
|
2324
|
+
const load = useCallback4(async () => {
|
|
2325
|
+
if (!services.account.hasSession) {
|
|
2326
|
+
setPhase({ name: "failed", message: "Approvals need a full sign-in", hint: "Run `tabbio login`; a personal MCP token cannot approve." });
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
setPhase({ name: "loading", since: Date.now() });
|
|
2330
|
+
try {
|
|
2331
|
+
const items = await services.loadPendingApprovals();
|
|
2332
|
+
if (mounted.current) setPhase({ name: "list", items });
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
const cli = toCliError(error);
|
|
2335
|
+
if (mounted.current) setPhase({ name: "failed", message: cli.message, hint: cli.hint });
|
|
2336
|
+
}
|
|
2337
|
+
}, [services]);
|
|
2338
|
+
useEffect6(() => {
|
|
2339
|
+
void load();
|
|
2340
|
+
}, [load]);
|
|
2341
|
+
const decide = useCallback4(
|
|
2342
|
+
async (decision) => {
|
|
2343
|
+
if (phase.name !== "card") return;
|
|
2344
|
+
if (decision === "leave") {
|
|
2345
|
+
setPhase({ name: "list", items: phase.items });
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
setPhase({ ...phase, busy: decision, error: null });
|
|
2349
|
+
try {
|
|
2350
|
+
const data = await services.decideApproval(phase.item.id, decision);
|
|
2351
|
+
if (!mounted.current) return;
|
|
2352
|
+
setPhase({
|
|
2353
|
+
name: "done",
|
|
2354
|
+
item: phase.item,
|
|
2355
|
+
decision,
|
|
2356
|
+
result: decision === "approve" ? data.mcp?.result ?? data.assistant?.text ?? null : null,
|
|
2357
|
+
durationMs: Date.now() - phase.shownAt
|
|
2358
|
+
});
|
|
2359
|
+
} catch (error) {
|
|
2360
|
+
if (mounted.current) setPhase({ ...phase, busy: null, error: toCliError(error).message });
|
|
2361
|
+
}
|
|
2362
|
+
},
|
|
2363
|
+
[phase, services]
|
|
2364
|
+
);
|
|
2365
|
+
useInput7(
|
|
2366
|
+
(input, key) => {
|
|
2367
|
+
if (phase.name === "done" && phase.decision !== "approve" && (key.return || key.escape || input === "q")) void load();
|
|
2368
|
+
else if (phase.name === "failed" && (key.escape || input === "q" || key.return)) onExit();
|
|
2369
|
+
else if (phase.name === "loading" && (key.escape || input === "q")) onExit();
|
|
2370
|
+
},
|
|
2371
|
+
{ isActive: phase.name === "done" && phase.decision !== "approve" || phase.name === "failed" || phase.name === "loading" }
|
|
2372
|
+
);
|
|
2373
|
+
const count = phase.name === "list" || phase.name === "card" ? phase.items.length : null;
|
|
2374
|
+
const header = /* @__PURE__ */ jsx19(Box18, { marginBottom: 1, children: /* @__PURE__ */ jsxs17(Text18, { children: [
|
|
2375
|
+
theme.bold("Approvals"),
|
|
2376
|
+
count !== null ? theme.dim(` ${theme.symbols.middot} ${count} waiting`) : ""
|
|
2377
|
+
] }) });
|
|
2378
|
+
switch (phase.name) {
|
|
2379
|
+
case "loading":
|
|
2380
|
+
return /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", width, children: [
|
|
2381
|
+
header,
|
|
2382
|
+
/* @__PURE__ */ jsx19(Working, { label: "Loading approvals\u2026", since: phase.since })
|
|
2383
|
+
] });
|
|
2384
|
+
case "failed":
|
|
2385
|
+
return /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", width, children: [
|
|
2386
|
+
header,
|
|
2387
|
+
/* @__PURE__ */ jsx19(Text18, { children: theme.error(`${theme.symbols.error} ${phase.message}`) }),
|
|
2388
|
+
phase.hint ? /* @__PURE__ */ jsx19(Text18, { wrap: "wrap", children: theme.dim(` ${theme.symbols.arrowRight} ${phase.hint}`) }) : null,
|
|
2389
|
+
/* @__PURE__ */ jsx19(Box18, { marginTop: 1, children: /* @__PURE__ */ jsx19(KeyHints, { width, hints: [{ key: "q", label: "back" }] }) })
|
|
2390
|
+
] });
|
|
2391
|
+
case "list":
|
|
2392
|
+
return /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", width, children: [
|
|
2393
|
+
header,
|
|
2394
|
+
/* @__PURE__ */ jsx19(
|
|
2395
|
+
SelectList,
|
|
2396
|
+
{
|
|
2397
|
+
items: phase.items.map((item) => ({
|
|
2398
|
+
id: item.id,
|
|
2399
|
+
label: `${theme.symbols.attention} ${sentenceCase(item.toolName)}`,
|
|
2400
|
+
description: summarizeArgs(argsOf(item), 40),
|
|
2401
|
+
meta: `${relativeTime(new Date(item.createdAt))} ${shortId(item.id)}`
|
|
2402
|
+
})),
|
|
2403
|
+
width,
|
|
2404
|
+
height: Math.max(3, height - 5),
|
|
2405
|
+
isActive: true,
|
|
2406
|
+
emptyText: "No approvals waiting",
|
|
2407
|
+
onSelect: (selected) => {
|
|
2408
|
+
const item = phase.items.find((i) => i.id === selected.id);
|
|
2409
|
+
if (item) setPhase({ name: "card", items: phase.items, item, shownAt: Date.now(), busy: null, error: null });
|
|
2410
|
+
},
|
|
2411
|
+
onCancel: onExit
|
|
2412
|
+
}
|
|
2413
|
+
)
|
|
2414
|
+
] });
|
|
2415
|
+
case "card":
|
|
2416
|
+
return /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", width, children: [
|
|
2417
|
+
header,
|
|
2418
|
+
/* @__PURE__ */ jsx19(
|
|
2419
|
+
ApprovalCard,
|
|
2420
|
+
{
|
|
2421
|
+
approvalId: phase.item.id,
|
|
2422
|
+
title: sentenceCase(phase.item.toolName),
|
|
2423
|
+
args: argsOf(phase.item),
|
|
2424
|
+
destructive: isDestructiveTool(phase.item.toolName),
|
|
2425
|
+
context: `Requested over MCP ${theme.symbols.middot} ${relativeTime(new Date(phase.item.createdAt))} ${theme.symbols.middot} profile ${services.profileName}`,
|
|
2426
|
+
busy: phase.busy,
|
|
2427
|
+
error: phase.error,
|
|
2428
|
+
isActive: !phase.busy,
|
|
2429
|
+
width,
|
|
2430
|
+
onDecision: (decision) => void decide(decision)
|
|
2431
|
+
}
|
|
2432
|
+
)
|
|
2433
|
+
] });
|
|
2434
|
+
case "done":
|
|
2435
|
+
return /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", width, children: [
|
|
2436
|
+
header,
|
|
2437
|
+
phase.decision === "approve" ? /* @__PURE__ */ jsx19(
|
|
2438
|
+
ResultPanel,
|
|
2439
|
+
{
|
|
2440
|
+
title: sentenceCase(phase.item.toolName),
|
|
2441
|
+
result: phase.result,
|
|
2442
|
+
durationMs: phase.durationMs,
|
|
2443
|
+
note: approvalOutcomeText("approve", sentenceCase(phase.item.toolName), phase.item.id, phase.durationMs),
|
|
2444
|
+
width,
|
|
2445
|
+
height: Math.max(6, height - 2),
|
|
2446
|
+
interactive: true,
|
|
2447
|
+
onBack: () => void load()
|
|
2448
|
+
}
|
|
2449
|
+
) : /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", children: [
|
|
2450
|
+
/* @__PURE__ */ jsx19(Text18, { children: approvalOutcomeText(phase.decision, sentenceCase(phase.item.toolName), phase.item.id, phase.durationMs) }),
|
|
2451
|
+
/* @__PURE__ */ jsx19(Box18, { marginTop: 1, children: /* @__PURE__ */ jsx19(KeyHints, { width, hints: [{ key: "enter", label: "back to approvals" }] }) })
|
|
2452
|
+
] })
|
|
2453
|
+
] });
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/ui/screens/ToolBrowser.tsx
|
|
2458
|
+
import { Box as Box20, Text as Text20, useInput as useInput9 } from "ink";
|
|
2459
|
+
import { memo as memo15, useMemo as useMemo6, useState as useState8 } from "react";
|
|
2460
|
+
|
|
2461
|
+
// src/ui/screens/ToolRun.tsx
|
|
2462
|
+
import { Box as Box19, Text as Text19, useInput as useInput8 } from "ink";
|
|
2463
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef6, useState as useState7 } from "react";
|
|
2464
|
+
import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
2465
|
+
function toolLabel(tool) {
|
|
2466
|
+
return tool.commandPath.join(" ") || tool.id;
|
|
2467
|
+
}
|
|
2468
|
+
function ToolRun({ tool, services, width, height, initialInput = {}, start, onExit }) {
|
|
2469
|
+
const [phase, setPhase] = useState7({ name: "form", input: initialInput });
|
|
2470
|
+
const mounted = useRef6(true);
|
|
2471
|
+
const controller = useRef6(null);
|
|
2472
|
+
useEffect7(
|
|
2473
|
+
() => () => {
|
|
2474
|
+
mounted.current = false;
|
|
2475
|
+
controller.current?.abort();
|
|
2476
|
+
},
|
|
2477
|
+
[]
|
|
2478
|
+
);
|
|
2479
|
+
const withDefaults = useCallback5(
|
|
2480
|
+
(input) => autoFilledProperties(tool, tool.inputSchema).includes("userId") && input.userId === void 0 && services.account.userId ? { ...input, userId: services.account.userId } : input,
|
|
2481
|
+
[tool, services.account.userId]
|
|
2482
|
+
);
|
|
2483
|
+
const run = useCallback5(
|
|
2484
|
+
async (input) => {
|
|
2485
|
+
const startedAt = Date.now();
|
|
2486
|
+
controller.current?.abort();
|
|
2487
|
+
const abort = new AbortController();
|
|
2488
|
+
controller.current = abort;
|
|
2489
|
+
setPhase({ name: "running", input, startedAt });
|
|
2490
|
+
try {
|
|
2491
|
+
const outcome = await services.callTool(tool.id, withDefaults(input), { signal: abort.signal });
|
|
2492
|
+
if (!mounted.current || abort.signal.aborted) return;
|
|
2493
|
+
if (!outcome.ok) setPhase({ name: "error", input, error: outcome.error, durationMs: outcome.durationMs });
|
|
2494
|
+
else if (outcome.approval) {
|
|
2495
|
+
setPhase(
|
|
2496
|
+
services.account.hasSession ? { name: "approval", input, approval: outcome.approval, requestedAt: Date.now(), busy: null, error: null } : { name: "left", input, approval: outcome.approval }
|
|
2497
|
+
);
|
|
2498
|
+
} else setPhase({ name: "result", input, result: outcome.result, durationMs: outcome.durationMs, requestId: outcome.requestId });
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
if (!mounted.current || abort.signal.aborted) return;
|
|
2501
|
+
setPhase({ name: "error", input, error: toCliError(error), durationMs: Date.now() - startedAt });
|
|
2502
|
+
}
|
|
2503
|
+
},
|
|
2504
|
+
[services, tool.id, withDefaults]
|
|
2505
|
+
);
|
|
2506
|
+
const started = useRef6(false);
|
|
2507
|
+
useEffect7(() => {
|
|
2508
|
+
if (started.current || start !== "run") return;
|
|
2509
|
+
started.current = true;
|
|
2510
|
+
void run(initialInput);
|
|
2511
|
+
}, [start, run, initialInput]);
|
|
2512
|
+
const decide = useCallback5(
|
|
2513
|
+
async (decision) => {
|
|
2514
|
+
if (phase.name !== "approval") return;
|
|
2515
|
+
const { approval, input, requestedAt } = phase;
|
|
2516
|
+
const title = sentenceCase(approval.toolName || tool.title);
|
|
2517
|
+
if (decision === "leave") {
|
|
2518
|
+
setPhase({ name: "left", input, approval });
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
setPhase({ ...phase, busy: decision, error: null });
|
|
2522
|
+
try {
|
|
2523
|
+
const data = await services.decideApproval(approval.approvalId, decision);
|
|
2524
|
+
if (!mounted.current) return;
|
|
2525
|
+
setPhase({
|
|
2526
|
+
name: "result",
|
|
2527
|
+
input,
|
|
2528
|
+
result: decision === "approve" ? data.mcp?.result ?? data.assistant?.text ?? null : null,
|
|
2529
|
+
durationMs: Date.now() - requestedAt,
|
|
2530
|
+
note: approvalOutcomeText(decision, title, approval.approvalId, Date.now() - requestedAt)
|
|
2531
|
+
});
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
if (!mounted.current) return;
|
|
2534
|
+
setPhase({ ...phase, busy: null, error: toCliError(error).message });
|
|
2535
|
+
}
|
|
2536
|
+
},
|
|
2537
|
+
[phase, services, tool.title]
|
|
2538
|
+
);
|
|
2539
|
+
useInput8(
|
|
2540
|
+
(input, key) => {
|
|
2541
|
+
if (phase.name === "running" && key.escape) {
|
|
2542
|
+
controller.current?.abort();
|
|
2543
|
+
setPhase({ name: "form", input: phase.input });
|
|
2544
|
+
} else if (phase.name === "error" || phase.name === "left") {
|
|
2545
|
+
if (key.escape || input === "q") onExit();
|
|
2546
|
+
else if (input === "r") void run(phase.input);
|
|
2547
|
+
else if (input === "e") setPhase({ name: "form", input: phase.input });
|
|
2548
|
+
}
|
|
2549
|
+
},
|
|
2550
|
+
{ isActive: phase.name === "running" || phase.name === "error" || phase.name === "left" }
|
|
2551
|
+
);
|
|
2552
|
+
const label = toolLabel(tool);
|
|
2553
|
+
const header = /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", marginBottom: 1, children: [
|
|
2554
|
+
/* @__PURE__ */ jsxs18(Text19, { wrap: "truncate-end", children: [
|
|
2555
|
+
theme.bold(label),
|
|
2556
|
+
" ",
|
|
2557
|
+
theme.dim(`${theme.symbols.middot} ${oneLine(tool.title)}`)
|
|
2558
|
+
] }),
|
|
2559
|
+
tool.description ? /* @__PURE__ */ jsx20(Text19, { wrap: "truncate-end", children: theme.dim(oneLine(tool.description)) }) : null
|
|
2560
|
+
] });
|
|
2561
|
+
switch (phase.name) {
|
|
2562
|
+
case "form":
|
|
2563
|
+
return /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", width, children: [
|
|
2564
|
+
header,
|
|
2565
|
+
/* @__PURE__ */ jsx20(
|
|
2566
|
+
Form,
|
|
2567
|
+
{
|
|
2568
|
+
initial: createFormState(tool.inputSchema, phase.input, { skip: autoFilledProperties(tool, tool.inputSchema) }),
|
|
2569
|
+
width,
|
|
2570
|
+
height: Math.max(3, height - 6),
|
|
2571
|
+
isActive: true,
|
|
2572
|
+
onSubmit: (input) => void run(input),
|
|
2573
|
+
onCancel: onExit
|
|
2574
|
+
}
|
|
2575
|
+
)
|
|
2576
|
+
] });
|
|
2577
|
+
case "running":
|
|
2578
|
+
return /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", width, children: [
|
|
2579
|
+
header,
|
|
2580
|
+
/* @__PURE__ */ jsx20(Working, { label: `Running ${label}${theme.symbols.ellipsis}`, since: phase.startedAt }),
|
|
2581
|
+
/* @__PURE__ */ jsx20(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx20(KeyHints, { width, hints: [{ key: "esc", label: "cancel" }] }) })
|
|
2582
|
+
] });
|
|
2583
|
+
case "approval":
|
|
2584
|
+
return /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", width, children: [
|
|
2585
|
+
header,
|
|
2586
|
+
/* @__PURE__ */ jsx20(
|
|
2587
|
+
ApprovalCard,
|
|
2588
|
+
{
|
|
2589
|
+
approvalId: phase.approval.approvalId,
|
|
2590
|
+
title: sentenceCase(phase.approval.toolName || tool.title),
|
|
2591
|
+
message: phase.approval.message,
|
|
2592
|
+
args: withDefaults(phase.input),
|
|
2593
|
+
destructive: isDestructiveTool(phase.approval.toolName || tool.id),
|
|
2594
|
+
context: `Writes to your account ${theme.symbols.middot} profile ${services.profileName}`,
|
|
2595
|
+
busy: phase.busy,
|
|
2596
|
+
error: phase.error,
|
|
2597
|
+
isActive: !phase.busy,
|
|
2598
|
+
width,
|
|
2599
|
+
onDecision: (decision) => void decide(decision)
|
|
2600
|
+
}
|
|
2601
|
+
)
|
|
2602
|
+
] });
|
|
2603
|
+
case "result":
|
|
2604
|
+
return /* @__PURE__ */ jsx20(
|
|
2605
|
+
ResultPanel,
|
|
2606
|
+
{
|
|
2607
|
+
title: label,
|
|
2608
|
+
result: phase.result,
|
|
2609
|
+
durationMs: phase.durationMs,
|
|
2610
|
+
requestId: phase.requestId,
|
|
2611
|
+
note: phase.note,
|
|
2612
|
+
width,
|
|
2613
|
+
height,
|
|
2614
|
+
interactive: true,
|
|
2615
|
+
onBack: onExit,
|
|
2616
|
+
onRerun: () => void run(phase.input),
|
|
2617
|
+
onEdit: () => setPhase({ name: "form", input: phase.input })
|
|
2618
|
+
}
|
|
2619
|
+
);
|
|
2620
|
+
case "error":
|
|
2621
|
+
return /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", width, children: [
|
|
2622
|
+
/* @__PURE__ */ jsx20(ResultPanel, { title: label, error: phase.error, durationMs: phase.durationMs, width, height, interactive: false }),
|
|
2623
|
+
/* @__PURE__ */ jsx20(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx20(KeyHints, { width, hints: [{ key: "r", label: "retry" }, { key: "e", label: "edit input" }, { key: "q", label: "back" }] }) })
|
|
2624
|
+
] });
|
|
2625
|
+
case "left":
|
|
2626
|
+
return /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", width, children: [
|
|
2627
|
+
header,
|
|
2628
|
+
/* @__PURE__ */ jsx20(Text19, { children: approvalOutcomeText("leave", sentenceCase(phase.approval.toolName || tool.title), phase.approval.approvalId) }),
|
|
2629
|
+
/* @__PURE__ */ jsx20(Text19, { wrap: "wrap", children: theme.dim(
|
|
2630
|
+
services.account.hasSession ? "Decide later in the Tabbio app or with `tabbio approvals`." : `Approve it in the Tabbio app, or run \`tabbio approvals wait ${shortId(phase.approval.approvalId, 12, 4)}\`.`
|
|
2631
|
+
) }),
|
|
2632
|
+
/* @__PURE__ */ jsx20(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx20(KeyHints, { width, hints: [{ key: "r", label: "run again" }, { key: "e", label: "edit input" }, { key: "q", label: "back" }] }) })
|
|
2633
|
+
] });
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// src/ui/screens/tool-browser-model.ts
|
|
2638
|
+
function initialBrowserState(opts = {}) {
|
|
2639
|
+
return { query: opts.query ?? "", cursor: 0, expanded: opts.group ? { [opts.group]: true } : {} };
|
|
2640
|
+
}
|
|
2641
|
+
function toolRank(tool, query) {
|
|
2642
|
+
const q = query.trim().toLowerCase();
|
|
2643
|
+
if (!q) return 0;
|
|
2644
|
+
const group = tool.group.toLowerCase();
|
|
2645
|
+
const path = tool.commandPath.join(" ").toLowerCase();
|
|
2646
|
+
const id = tool.id.toLowerCase();
|
|
2647
|
+
if (q === group || q === path || q === id) return 0;
|
|
2648
|
+
if (group.startsWith(q) || tool.action.toLowerCase().startsWith(q) || path.startsWith(q) || id.startsWith(q)) return 1;
|
|
2649
|
+
const ranks = [rankMatch(q, path), rankMatch(q, id), rankMatch(q, tool.title, tool.description)].filter(
|
|
2650
|
+
(r) => r !== null
|
|
2651
|
+
);
|
|
2652
|
+
return ranks.length ? Math.max(2, Math.min(...ranks)) : null;
|
|
2653
|
+
}
|
|
2654
|
+
function browserRows(tools, state) {
|
|
2655
|
+
const visible = tools.filter((t) => !t.hidden);
|
|
2656
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2657
|
+
for (const tool of visible) {
|
|
2658
|
+
const entry = groups.get(tool.group) ?? { total: 0, hits: [] };
|
|
2659
|
+
entry.total += 1;
|
|
2660
|
+
const rank = toolRank(tool, state.query);
|
|
2661
|
+
if (rank !== null) entry.hits.push({ tool, rank });
|
|
2662
|
+
groups.set(tool.group, entry);
|
|
2663
|
+
}
|
|
2664
|
+
const filtering = state.query.trim().length > 0;
|
|
2665
|
+
const rows = [];
|
|
2666
|
+
const best = (group) => Math.min(...groups.get(group)?.hits.map((h) => h.rank) ?? [Infinity]);
|
|
2667
|
+
const names = [...groups.keys()].sort((a, b) => (filtering ? best(a) - best(b) : 0) || a.localeCompare(b));
|
|
2668
|
+
for (const group of names) {
|
|
2669
|
+
const entry = groups.get(group);
|
|
2670
|
+
if (filtering && entry.hits.length === 0) continue;
|
|
2671
|
+
const expanded = state.expanded[group] ?? filtering;
|
|
2672
|
+
rows.push({ kind: "group", group, matched: entry.hits.length, total: entry.total, expanded });
|
|
2673
|
+
if (!expanded) continue;
|
|
2674
|
+
const hits = filtering ? [...entry.hits].sort((a, b) => a.rank - b.rank) : entry.hits;
|
|
2675
|
+
for (const hit of hits) rows.push({ kind: "tool", tool: hit.tool, group });
|
|
2676
|
+
}
|
|
2677
|
+
return rows;
|
|
2678
|
+
}
|
|
2679
|
+
function browserReducer(state, action, rows) {
|
|
2680
|
+
switch (action.type) {
|
|
2681
|
+
case "type":
|
|
2682
|
+
return { ...state, query: state.query + action.text, cursor: 0, expanded: {} };
|
|
2683
|
+
case "backspace":
|
|
2684
|
+
return { ...state, query: [...state.query].slice(0, -1).join(""), cursor: 0, expanded: {} };
|
|
2685
|
+
case "clear":
|
|
2686
|
+
return { ...state, query: "", cursor: 0, expanded: {} };
|
|
2687
|
+
case "move":
|
|
2688
|
+
return { ...state, cursor: Math.max(0, Math.min(rows.length - 1, state.cursor + action.delta)) };
|
|
2689
|
+
case "fold": {
|
|
2690
|
+
const row = rows[state.cursor];
|
|
2691
|
+
if (!row) return state;
|
|
2692
|
+
const group = row.group;
|
|
2693
|
+
const expanded = { ...state.expanded, [group]: action.expand };
|
|
2694
|
+
const groupIndex = rows.findIndex((r) => r.kind === "group" && r.group === group);
|
|
2695
|
+
return { ...state, expanded, cursor: !action.expand && groupIndex >= 0 ? groupIndex : state.cursor };
|
|
2696
|
+
}
|
|
2697
|
+
default:
|
|
2698
|
+
return state;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
function applyBrowserAction(tools, state, action) {
|
|
2702
|
+
const next = browserReducer(state, action, browserRows(tools, state));
|
|
2703
|
+
if (action.type !== "type" && action.type !== "backspace" && action.type !== "clear") return next;
|
|
2704
|
+
if (!next.query) return next;
|
|
2705
|
+
const index = browserRows(tools, next).findIndex((row) => row.kind === "tool");
|
|
2706
|
+
return index >= 0 ? { ...next, cursor: index } : next;
|
|
2707
|
+
}
|
|
2708
|
+
function browserKeyAction(state, input, key, rows) {
|
|
2709
|
+
const row = rows[Math.min(state.cursor, rows.length - 1)];
|
|
2710
|
+
if (key.upArrow) return { type: "move", delta: -1 };
|
|
2711
|
+
if (key.downArrow) return { type: "move", delta: 1 };
|
|
2712
|
+
if (key.pageUp) return { type: "move", delta: -10 };
|
|
2713
|
+
if (key.pageDown) return { type: "move", delta: 10 };
|
|
2714
|
+
if (key.leftArrow) return { type: "fold", expand: false };
|
|
2715
|
+
if (key.rightArrow) return { type: "fold", expand: true };
|
|
2716
|
+
if (key.return) {
|
|
2717
|
+
if (!row) return null;
|
|
2718
|
+
if (row.kind === "tool") return { type: "open", tool: row.tool };
|
|
2719
|
+
return { type: "fold", expand: !row.expanded };
|
|
2720
|
+
}
|
|
2721
|
+
if (key.escape) return state.query ? { type: "clear" } : { type: "exit" };
|
|
2722
|
+
if (key.backspace || key.delete) return { type: "backspace" };
|
|
2723
|
+
if (key.ctrl && input === "u") return { type: "clear" };
|
|
2724
|
+
if (key.ctrl || key.meta || key.tab) return null;
|
|
2725
|
+
if (input === "q" && !state.query) return { type: "exit" };
|
|
2726
|
+
return input ? { type: "type", text: input } : null;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
// src/ui/screens/ToolBrowser.tsx
|
|
2730
|
+
import { jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
2731
|
+
var TWO_PANE_MIN = fromTerminal(90);
|
|
2732
|
+
function badge(tool) {
|
|
2733
|
+
if (tool.kind === "workflow") return theme.dim("workflow");
|
|
2734
|
+
if (tool.kind === "agent") return theme.dim("agent");
|
|
2735
|
+
if (tool.annotations.destructiveHint === true) return theme.danger("write");
|
|
2736
|
+
return tool.readOnly ? theme.dim("read") : "write";
|
|
2737
|
+
}
|
|
2738
|
+
var ListRow = memo15(function ListRow2({ row, selected, width }) {
|
|
2739
|
+
const pointer = selected ? theme.accent(theme.symbols.pointer) : " ";
|
|
2740
|
+
if (row.kind === "group") {
|
|
2741
|
+
const glyph2 = row.expanded ? theme.symbols.expanded : theme.symbols.collapsed;
|
|
2742
|
+
const count = theme.dim(row.matched === row.total ? String(row.total) : `${row.matched} of ${row.total}`);
|
|
2743
|
+
const name = truncate(row.group, Math.max(4, width - 12));
|
|
2744
|
+
const fill = Math.max(1, width - 4 - displayWidth(name) - displayWidth(count));
|
|
2745
|
+
return /* @__PURE__ */ jsxs19(Text20, { wrap: "truncate-end", children: [
|
|
2746
|
+
pointer,
|
|
2747
|
+
" ",
|
|
2748
|
+
theme.dim(glyph2),
|
|
2749
|
+
" ",
|
|
2750
|
+
selected ? theme.accent(theme.bold(name)) : theme.bold(name),
|
|
2751
|
+
" ".repeat(fill),
|
|
2752
|
+
count
|
|
2753
|
+
] });
|
|
2754
|
+
}
|
|
2755
|
+
const label = row.tool.kind === "workflow" ? row.tool.action : row.tool.commandPath.join(" ");
|
|
2756
|
+
const tag = badge(row.tool);
|
|
2757
|
+
const labelWidth = Math.max(4, width - 4 - displayWidth(tag) - 1);
|
|
2758
|
+
const text = padEnd(truncate(label, labelWidth), labelWidth);
|
|
2759
|
+
return /* @__PURE__ */ jsxs19(Text20, { wrap: "truncate-end", children: [
|
|
2760
|
+
pointer,
|
|
2761
|
+
" ",
|
|
2762
|
+
selected ? theme.accent(text) : text,
|
|
2763
|
+
" ",
|
|
2764
|
+
tag
|
|
2765
|
+
] });
|
|
2766
|
+
});
|
|
2767
|
+
var ToolDetail = memo15(function ToolDetail2({ tool, width, height }) {
|
|
2768
|
+
const flags = useMemo6(() => describeSchema(tool.inputSchema), [tool]);
|
|
2769
|
+
const example = useMemo6(() => exampleInvocation(tool, tool.inputSchema), [tool]);
|
|
2770
|
+
const flagWidth = Math.min(28, Math.max(8, ...flags.map((f) => displayWidth(`${f.flag} <${f.type}>`))));
|
|
2771
|
+
const descLines = Math.max(1, Math.min(4, height - flags.length - 8));
|
|
2772
|
+
const lines = wrapWords(tool.description, width, descLines);
|
|
2773
|
+
const shownFlags = flags.slice(0, Math.max(1, height - 7 - lines.length));
|
|
2774
|
+
return /* @__PURE__ */ jsxs19(Box20, { flexDirection: "column", width, children: [
|
|
2775
|
+
/* @__PURE__ */ jsxs19(Box20, { width, children: [
|
|
2776
|
+
/* @__PURE__ */ jsx21(Box20, { flexGrow: 1, children: /* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: theme.bold(tool.commandPath.join(" ")) }) }),
|
|
2777
|
+
/* @__PURE__ */ jsx21(Text20, { children: badge(tool) })
|
|
2778
|
+
] }),
|
|
2779
|
+
/* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: oneLine(tool.title) }),
|
|
2780
|
+
/* @__PURE__ */ jsx21(Text20, { children: " " }),
|
|
2781
|
+
lines.map((line, i) => /* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: theme.dim(line) }, i)),
|
|
2782
|
+
/* @__PURE__ */ jsx21(Text20, { children: " " }),
|
|
2783
|
+
/* @__PURE__ */ jsx21(Text20, { children: theme.dim("FLAGS") }),
|
|
2784
|
+
flags.length === 0 ? /* @__PURE__ */ jsx21(Text20, { children: theme.dim(" none") }) : null,
|
|
2785
|
+
shownFlags.map((flag) => /* @__PURE__ */ jsxs19(Text20, { wrap: "truncate-end", children: [
|
|
2786
|
+
" ",
|
|
2787
|
+
padEnd(truncate(`${flag.flag} <${flag.type}>`, flagWidth), flagWidth),
|
|
2788
|
+
" ",
|
|
2789
|
+
flag.required ? theme.attention("required") : " ",
|
|
2790
|
+
" ",
|
|
2791
|
+
theme.dim(flag.description)
|
|
2792
|
+
] }, flag.flag)),
|
|
2793
|
+
flags.length > shownFlags.length ? /* @__PURE__ */ jsx21(Text20, { children: theme.dim(` +${flags.length - shownFlags.length} more`) }) : null,
|
|
2794
|
+
/* @__PURE__ */ jsx21(Text20, { children: " " }),
|
|
2795
|
+
/* @__PURE__ */ jsx21(Text20, { children: theme.dim("EXAMPLE") }),
|
|
2796
|
+
/* @__PURE__ */ jsxs19(Text20, { wrap: "truncate-end", children: [
|
|
2797
|
+
" ",
|
|
2798
|
+
example
|
|
2799
|
+
] })
|
|
2800
|
+
] });
|
|
2801
|
+
});
|
|
2802
|
+
function ToolBrowser(props) {
|
|
2803
|
+
const { tools, services, width, height, onExit } = props;
|
|
2804
|
+
const [state, setState] = useState8(() => {
|
|
2805
|
+
const initial = initialBrowserState({ query: props.initialQuery, group: props.initialGroup });
|
|
2806
|
+
return initial.query ? applyBrowserAction(tools, { ...initial, query: "" }, { type: "type", text: initial.query }) : initial;
|
|
2807
|
+
});
|
|
2808
|
+
const [running, setRunning] = useState8(null);
|
|
2809
|
+
const rows = useMemo6(() => browserRows(tools, state), [tools, state]);
|
|
2810
|
+
const cursor = Math.min(state.cursor, Math.max(0, rows.length - 1));
|
|
2811
|
+
const current = rows[cursor];
|
|
2812
|
+
useInput9(
|
|
2813
|
+
(input, key) => {
|
|
2814
|
+
const outcome = browserKeyAction({ ...state, cursor }, input, key, rows);
|
|
2815
|
+
if (!outcome) return;
|
|
2816
|
+
if (outcome.type === "exit") onExit();
|
|
2817
|
+
else if (outcome.type === "open") setRunning(outcome.tool);
|
|
2818
|
+
else setState((s) => applyBrowserAction(tools, { ...s, cursor }, outcome));
|
|
2819
|
+
},
|
|
2820
|
+
{ isActive: !running }
|
|
2821
|
+
);
|
|
2822
|
+
if (running) {
|
|
2823
|
+
return /* @__PURE__ */ jsx21(ToolRun, { tool: running, services, width, height, start: "form", onExit: () => setRunning(null) });
|
|
2824
|
+
}
|
|
2825
|
+
const visibleCount = tools.filter((t) => !t.hidden).length;
|
|
2826
|
+
const groupCount = new Set(tools.filter((t) => !t.hidden).map((t) => t.group)).size;
|
|
2827
|
+
const twoPane = width >= TWO_PANE_MIN;
|
|
2828
|
+
const leftWidth = twoPane ? Math.min(46, Math.floor(width * 0.42)) : width;
|
|
2829
|
+
const rightWidth = width - leftWidth - 3;
|
|
2830
|
+
const listHeight = Math.max(3, height - (twoPane ? 5 : 9));
|
|
2831
|
+
const { start, end } = windowAround(rows.length, cursor, listHeight);
|
|
2832
|
+
const filter = `${theme.dim("filter")} ${theme.accent(theme.symbols.pointer)} ${state.query}${theme.accent(theme.symbols.cursor)}`;
|
|
2833
|
+
const title = `${theme.bold("Tools")} ${theme.dim(`${theme.symbols.middot} ${visibleCount} in ${groupCount} groups`)}`;
|
|
2834
|
+
const rule = theme.dim(
|
|
2835
|
+
twoPane ? `${theme.symbols.line.repeat(leftWidth + 1)}\u252C${theme.symbols.line.repeat(Math.max(0, rightWidth + 1))}` : theme.symbols.line.repeat(width)
|
|
2836
|
+
);
|
|
2837
|
+
const detailTool = current?.kind === "tool" ? current.tool : null;
|
|
2838
|
+
return /* @__PURE__ */ jsxs19(Box20, { flexDirection: "column", width, children: [
|
|
2839
|
+
/* @__PURE__ */ jsxs19(Box20, { width, children: [
|
|
2840
|
+
/* @__PURE__ */ jsx21(Box20, { flexGrow: 1, children: /* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: title }) }),
|
|
2841
|
+
/* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: filter })
|
|
2842
|
+
] }),
|
|
2843
|
+
/* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: theme.unicode ? rule : theme.dim("-".repeat(width)) }),
|
|
2844
|
+
/* @__PURE__ */ jsxs19(Box20, { width, children: [
|
|
2845
|
+
/* @__PURE__ */ jsxs19(Box20, { flexDirection: "column", width: leftWidth, children: [
|
|
2846
|
+
rows.length === 0 ? /* @__PURE__ */ jsx21(Text20, { children: theme.dim(` No tools match "${state.query}"`) }) : null,
|
|
2847
|
+
rows.slice(start, end).map((row, i) => /* @__PURE__ */ jsx21(
|
|
2848
|
+
ListRow,
|
|
2849
|
+
{
|
|
2850
|
+
row,
|
|
2851
|
+
selected: start + i === cursor,
|
|
2852
|
+
width: leftWidth
|
|
2853
|
+
},
|
|
2854
|
+
row.kind === "group" ? `g:${row.group}` : `t:${row.tool.id}`
|
|
2855
|
+
))
|
|
2856
|
+
] }),
|
|
2857
|
+
twoPane ? /* @__PURE__ */ jsx21(Box20, { flexDirection: "column", marginLeft: 1, paddingLeft: 1, borderStyle: theme.unicode ? "single" : "classic", borderTop: false, borderRight: false, borderBottom: false, borderDimColor: true, width: rightWidth + 2, children: detailTool ? /* @__PURE__ */ jsx21(ToolDetail, { tool: detailTool, width: rightWidth, height: listHeight }) : /* @__PURE__ */ jsx21(Text20, { children: theme.dim(current?.kind === "group" ? `${current.group}: enter or \u2192 to open the group` : "Select a tool") }) }) : null
|
|
2858
|
+
] }),
|
|
2859
|
+
!twoPane && detailTool ? /* @__PURE__ */ jsx21(Box20, { marginTop: 1, children: /* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: theme.dim(oneLine(detailTool.description || detailTool.title)) }) }) : null,
|
|
2860
|
+
/* @__PURE__ */ jsx21(Text20, { wrap: "truncate-end", children: theme.dim(theme.symbols.line.repeat(width)) }),
|
|
2861
|
+
/* @__PURE__ */ jsx21(
|
|
2862
|
+
KeyHints,
|
|
2863
|
+
{
|
|
2864
|
+
width,
|
|
2865
|
+
hints: [
|
|
2866
|
+
{ key: theme.symbols.upDown, label: "move" },
|
|
2867
|
+
{ key: theme.symbols.leftRight, label: "fold" },
|
|
2868
|
+
{ key: "enter", label: "run" },
|
|
2869
|
+
{ key: "type", label: "filter" },
|
|
2870
|
+
{ key: "esc", label: state.query ? "clear" : "back" }
|
|
2871
|
+
]
|
|
2872
|
+
}
|
|
2873
|
+
)
|
|
2874
|
+
] });
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
// src/ui/screens/home/home-model.ts
|
|
2878
|
+
var emptyHomeData = { plan: null, pendingApprovals: null, recent: null };
|
|
2879
|
+
function homeCommands(data, toolCount) {
|
|
2880
|
+
const waiting = data.pendingApprovals;
|
|
2881
|
+
return [
|
|
2882
|
+
{ name: "chat", description: "Chat with Tabbio" },
|
|
2883
|
+
{ name: "tools", description: toolCount ? `Browse ${toolCount} tools` : "Browse every tool" },
|
|
2884
|
+
{ name: "approvals", description: waiting === null ? "Approve or reject actions" : waiting > 0 ? `${waiting} waiting` : "None waiting" },
|
|
2885
|
+
{ name: "status", description: "Account, tokens, catalog" },
|
|
2886
|
+
{ name: "help", description: "Keys and commands" },
|
|
2887
|
+
{ name: "exit", description: "Quit tabbio" }
|
|
2888
|
+
];
|
|
2889
|
+
}
|
|
2890
|
+
function isHomeCommand(name) {
|
|
2891
|
+
return ["chat", "tools", "approvals", "status", "help", "exit", "quit"].includes(name);
|
|
2892
|
+
}
|
|
2893
|
+
var HOME_HELP_ROWS = [
|
|
2894
|
+
["type + enter", "Start a chat with that message"],
|
|
2895
|
+
["/", "Command palette (chat, tools, approvals, status, help, exit)"],
|
|
2896
|
+
["tab", "Jump to recent chats"],
|
|
2897
|
+
["esc", "Close, go back, or interrupt"],
|
|
2898
|
+
["ctrl+j", "New line in the composer (or \\ then enter)"],
|
|
2899
|
+
["\u2191 \u2193", "Composer history, or move in lists"],
|
|
2900
|
+
["ctrl+c \xD72", "Quit"]
|
|
2901
|
+
];
|
|
2902
|
+
|
|
2903
|
+
// src/ui/screens/home/HomeView.tsx
|
|
2904
|
+
import { Box as Box23, Text as Text23 } from "ink";
|
|
2905
|
+
import { memo as memo17 } from "react";
|
|
2906
|
+
|
|
2907
|
+
// src/ui/components/Header.tsx
|
|
2908
|
+
import { Box as Box22, Text as Text22 } from "ink";
|
|
2909
|
+
import { memo as memo16 } from "react";
|
|
2910
|
+
|
|
2911
|
+
// src/ui/components/Wordmark.tsx
|
|
2912
|
+
import { Box as Box21, Text as Text21 } from "ink";
|
|
2913
|
+
import Gradient from "ink-gradient";
|
|
2914
|
+
import { jsx as jsx22 } from "react/jsx-runtime";
|
|
2915
|
+
var WORDMARK_ROWS = [
|
|
2916
|
+
" \u2588 \u2588 \u2588 \u2580",
|
|
2917
|
+
"\u2580\u2588\u2580 \u2584\u2580\u2580\u2588 \u2588\u2580\u2580\u2584 \u2588\u2580\u2580\u2584 \u2588 \u2584\u2580\u2580\u2584",
|
|
2918
|
+
" \u2588\u2584 \u2580\u2584\u2584\u2588 \u2588\u2584\u2584\u2580 \u2588\u2584\u2584\u2580 \u2588 \u2580\u2584\u2584\u2580"
|
|
2919
|
+
];
|
|
2920
|
+
var WORDMARK_WIDTH = 25;
|
|
2921
|
+
function Wordmark() {
|
|
2922
|
+
if (!theme.unicode) return /* @__PURE__ */ jsx22(Text21, { children: theme.accentBold("tabbio") });
|
|
2923
|
+
const rows = WORDMARK_ROWS.map((row) => row.padEnd(WORDMARK_WIDTH));
|
|
2924
|
+
const gradient = theme.wordmarkGradient;
|
|
2925
|
+
if (gradient) {
|
|
2926
|
+
return /* @__PURE__ */ jsx22(Gradient, { colors: [...gradient], children: /* @__PURE__ */ jsx22(Text21, { children: rows.join("\n") }) });
|
|
2927
|
+
}
|
|
2928
|
+
return /* @__PURE__ */ jsx22(Box21, { flexDirection: "column", children: rows.map((row, i) => /* @__PURE__ */ jsx22(Text21, { children: theme.accent(row) }, i)) });
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
// src/ui/components/Header.tsx
|
|
2932
|
+
import { jsx as jsx23, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
2933
|
+
var WIDE_HEADER_MIN = fromTerminal(60);
|
|
2934
|
+
function oneLineHeader({ version, profile, account }) {
|
|
2935
|
+
const sep = theme.dim(` ${theme.symbols.middot} `);
|
|
2936
|
+
const who = account?.name || account?.email;
|
|
2937
|
+
const parts = [
|
|
2938
|
+
theme.accentBold(`tabbio ${version}`),
|
|
2939
|
+
who ?? null,
|
|
2940
|
+
account?.plan ? capitalize(account.plan) : null,
|
|
2941
|
+
theme.dim(profile)
|
|
2942
|
+
].filter((p) => Boolean(p));
|
|
2943
|
+
return parts.join(sep);
|
|
2944
|
+
}
|
|
2945
|
+
function capitalize(value) {
|
|
2946
|
+
return value ? value.charAt(0).toUpperCase() + value.slice(1) : value;
|
|
2947
|
+
}
|
|
2948
|
+
var Header = memo16(function Header2(props) {
|
|
2949
|
+
const { version, width, tagline = "Your career, from the terminal" } = props;
|
|
2950
|
+
if (width < WIDE_HEADER_MIN || !theme.unicode) {
|
|
2951
|
+
return /* @__PURE__ */ jsx23(Text22, { wrap: "truncate-end", children: oneLineHeader(props) });
|
|
2952
|
+
}
|
|
2953
|
+
const showTagline = width >= WORDMARK_WIDTH + 3 + tagline.length;
|
|
2954
|
+
return /* @__PURE__ */ jsxs20(Box22, { children: [
|
|
2955
|
+
/* @__PURE__ */ jsx23(Wordmark, {}),
|
|
2956
|
+
/* @__PURE__ */ jsxs20(Box22, { flexDirection: "column", marginLeft: 3, children: [
|
|
2957
|
+
/* @__PURE__ */ jsx23(Text22, { children: " " }),
|
|
2958
|
+
/* @__PURE__ */ jsx23(Text22, { children: theme.bold(`tabbio ${version}`) }),
|
|
2959
|
+
/* @__PURE__ */ jsx23(Text22, { wrap: "truncate-end", children: showTagline ? theme.dim(tagline) : "" })
|
|
2960
|
+
] })
|
|
2961
|
+
] });
|
|
2962
|
+
});
|
|
2963
|
+
|
|
2964
|
+
// src/ui/screens/home/HomeView.tsx
|
|
2965
|
+
import { jsx as jsx24, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
2966
|
+
var TWO_COLUMN_MIN = fromTerminal(100);
|
|
2967
|
+
function AccountLine({ services, plan, width }) {
|
|
2968
|
+
const { account } = services;
|
|
2969
|
+
const sep = theme.dim(` ${theme.symbols.middot} `);
|
|
2970
|
+
const who = account.signedIn ? [account.name, account.email, plan ? capitalize(plan) : null].filter(Boolean).join(sep) || theme.dim("personal MCP token") : theme.attention("Not signed in");
|
|
2971
|
+
const left = `${account.signedIn ? theme.success(theme.symbols.dot) : theme.attention(theme.symbols.dot)} ${who}`;
|
|
2972
|
+
const right = theme.dim(`profile ${services.profileName} ${theme.symbols.middot} ${services.apiHost}`);
|
|
2973
|
+
const fill = width - displayWidth(left) - displayWidth(right);
|
|
2974
|
+
return /* @__PURE__ */ jsx24(Text23, { wrap: "truncate-end", children: fill >= 2 ? `${left}${" ".repeat(fill)}${right}` : left });
|
|
2975
|
+
}
|
|
2976
|
+
var QuickActions = memo17(function QuickActions2({ commands, width, pending }) {
|
|
2977
|
+
const nameWidth = 13;
|
|
2978
|
+
return /* @__PURE__ */ jsxs21(Box23, { flexDirection: "column", width, children: [
|
|
2979
|
+
/* @__PURE__ */ jsx24(Text23, { children: theme.dim("START") }),
|
|
2980
|
+
commands.filter((c) => c.name !== "exit").map((c) => /* @__PURE__ */ jsxs21(Text23, { wrap: "truncate-end", children: [
|
|
2981
|
+
theme.accent(padEnd(`/${c.name}`, nameWidth)),
|
|
2982
|
+
truncate(c.description, Math.max(4, width - nameWidth - 3)),
|
|
2983
|
+
c.name === "approvals" && pending ? ` ${theme.attention(theme.symbols.dot)}` : ""
|
|
2984
|
+
] }, c.name))
|
|
2985
|
+
] });
|
|
2986
|
+
});
|
|
2987
|
+
function Recent(props) {
|
|
2988
|
+
const { data, focus, width, rows, services } = props;
|
|
2989
|
+
const title = /* @__PURE__ */ jsx24(Text23, { children: theme.dim("RECENT CHATS") });
|
|
2990
|
+
if (!services.account.hasSession) {
|
|
2991
|
+
return /* @__PURE__ */ jsxs21(Box23, { flexDirection: "column", width, children: [
|
|
2992
|
+
title,
|
|
2993
|
+
/* @__PURE__ */ jsx24(Text23, { wrap: "truncate-end", children: theme.dim("Sign in with `tabbio login` to see your chats") })
|
|
2994
|
+
] });
|
|
2995
|
+
}
|
|
2996
|
+
if (data.recent === null) {
|
|
2997
|
+
return /* @__PURE__ */ jsxs21(Box23, { flexDirection: "column", width, children: [
|
|
2998
|
+
title,
|
|
2999
|
+
/* @__PURE__ */ jsx24(Text23, { children: theme.dim(data.error ? `Could not load: ${data.error}` : `Loading${theme.symbols.ellipsis}`) })
|
|
3000
|
+
] });
|
|
3001
|
+
}
|
|
3002
|
+
return /* @__PURE__ */ jsxs21(Box23, { flexDirection: "column", width, children: [
|
|
3003
|
+
title,
|
|
3004
|
+
data.recent.length === 0 ? /* @__PURE__ */ jsx24(Text23, { children: theme.dim("No chats yet. Type below to start one.") }) : null,
|
|
3005
|
+
data.recent.length > 0 ? /* @__PURE__ */ jsx24(
|
|
3006
|
+
SelectList,
|
|
3007
|
+
{
|
|
3008
|
+
items: data.recent.map((r) => ({ id: r.id, label: r.title, meta: r.when })),
|
|
3009
|
+
width,
|
|
3010
|
+
height: rows,
|
|
3011
|
+
isActive: focus === "recent",
|
|
3012
|
+
filterable: false,
|
|
3013
|
+
showHints: false,
|
|
3014
|
+
onSelect: (item) => props.onRecentSelect(item.id, item.label),
|
|
3015
|
+
onCancel: props.onRecentCancel
|
|
3016
|
+
}
|
|
3017
|
+
) : null
|
|
3018
|
+
] });
|
|
3019
|
+
}
|
|
3020
|
+
function HomeView(props) {
|
|
3021
|
+
const { services, data, commands, matches, width, height } = props;
|
|
3022
|
+
const wide = width >= TWO_COLUMN_MIN;
|
|
3023
|
+
const tall = height >= 26;
|
|
3024
|
+
const showWordmark = width >= WIDE_HEADER_MIN && tall;
|
|
3025
|
+
const showAccount = width >= WIDE_HEADER_MIN;
|
|
3026
|
+
const recentRows = wide ? 5 : height >= 30 ? 3 : 0;
|
|
3027
|
+
const colWidth = wide ? Math.floor((width - 4) / 2) : width;
|
|
3028
|
+
const account = { name: services.account.name, email: services.account.email, plan: data.plan };
|
|
3029
|
+
return /* @__PURE__ */ jsxs21(Box23, { flexDirection: "column", width, children: [
|
|
3030
|
+
showWordmark ? /* @__PURE__ */ jsx24(Header, { version: services.version, profile: services.profileName, width, account }) : /* @__PURE__ */ jsx24(Header, { version: services.version, profile: services.profileName, width: Math.min(width, WIDE_HEADER_MIN - 1), account }),
|
|
3031
|
+
showAccount ? /* @__PURE__ */ jsx24(Box23, { marginTop: 1, children: /* @__PURE__ */ jsx24(AccountLine, { services, plan: data.plan, width }) }) : null,
|
|
3032
|
+
/* @__PURE__ */ jsxs21(Box23, { marginTop: 1, flexDirection: wide ? "row" : "column", width, children: [
|
|
3033
|
+
/* @__PURE__ */ jsx24(QuickActions, { commands, width: colWidth, pending: data.pendingApprovals }),
|
|
3034
|
+
recentRows > 0 ? /* @__PURE__ */ jsx24(Box23, { marginLeft: wide ? 4 : 0, marginTop: wide ? 0 : 1, children: /* @__PURE__ */ jsx24(Recent, { ...props, width: colWidth, rows: recentRows }) }) : null
|
|
3035
|
+
] }),
|
|
3036
|
+
matches.length > 0 ? /* @__PURE__ */ jsx24(Box23, { marginTop: 1, children: /* @__PURE__ */ jsx24(Palette, { commands: matches, selected: props.paletteIndex, width }) }) : null,
|
|
3037
|
+
/* @__PURE__ */ jsx24(Box23, { marginTop: 1, children: /* @__PURE__ */ jsx24(
|
|
3038
|
+
TextInput,
|
|
3039
|
+
{
|
|
3040
|
+
editor: props.editor,
|
|
3041
|
+
dispatch: props.dispatchEditor,
|
|
3042
|
+
onSubmit: props.onSubmit,
|
|
3043
|
+
onKey: props.onComposerKey,
|
|
3044
|
+
isActive: props.focus === "composer",
|
|
3045
|
+
width,
|
|
3046
|
+
placeholder: width < WIDE_HEADER_MIN ? "Ask Tabbio anything, or / for commands" : "Ask Tabbio anything, or type / for commands",
|
|
3047
|
+
boxed: true
|
|
3048
|
+
}
|
|
3049
|
+
) }),
|
|
3050
|
+
props.notice ? /* @__PURE__ */ jsx24(Text23, { wrap: "truncate-end", children: props.notice }) : null,
|
|
3051
|
+
/* @__PURE__ */ jsx24(
|
|
3052
|
+
KeyHints,
|
|
3053
|
+
{
|
|
3054
|
+
width,
|
|
3055
|
+
hints: [
|
|
3056
|
+
{ key: "enter", label: "send" },
|
|
3057
|
+
{ key: "/", label: "commands" },
|
|
3058
|
+
...services.account.hasSession && recentRows > 0 ? [{ key: "tab", label: "recent chats" }] : [],
|
|
3059
|
+
{ key: "ctrl+c", label: "quit" }
|
|
3060
|
+
]
|
|
3061
|
+
}
|
|
3062
|
+
)
|
|
3063
|
+
] });
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
// src/ui/screens/home/Home.tsx
|
|
3067
|
+
import { jsx as jsx25, jsxs as jsxs22 } from "react/jsx-runtime";
|
|
3068
|
+
var DOUBLE_PRESS_MS2 = 2e3;
|
|
3069
|
+
function BackKeys({ onBack }) {
|
|
3070
|
+
useInput10((input, key) => {
|
|
3071
|
+
if (key.escape || input === "q" || key.return) onBack();
|
|
3072
|
+
});
|
|
3073
|
+
return null;
|
|
3074
|
+
}
|
|
3075
|
+
function Home(props) {
|
|
3076
|
+
const { services, width, height, onExit, mode = "seeker" } = props;
|
|
3077
|
+
const [route, setRoute] = useState9({ name: "home" });
|
|
3078
|
+
const [data, setData] = useState9(props.initialData ?? emptyHomeData);
|
|
3079
|
+
const [editor, dispatchEditor] = useReducer4(editorReducer, emptyEditor);
|
|
3080
|
+
const [paletteIndex, setPaletteIndex] = useState9(0);
|
|
3081
|
+
const [focus, setFocus] = useState9("composer");
|
|
3082
|
+
const [notice, setNotice] = useState9(null);
|
|
3083
|
+
const lastCtrlC = useRef7(0);
|
|
3084
|
+
const mounted = useRef7(true);
|
|
3085
|
+
useEffect8(
|
|
3086
|
+
() => () => {
|
|
3087
|
+
mounted.current = false;
|
|
3088
|
+
},
|
|
3089
|
+
[]
|
|
3090
|
+
);
|
|
3091
|
+
const loadData = useCallback6(async () => {
|
|
3092
|
+
if (props.initialData || !services.account.hasSession) return;
|
|
3093
|
+
const [plan, approvals, threads] = await Promise.allSettled([
|
|
3094
|
+
services.loadPlan(),
|
|
3095
|
+
services.loadPendingApprovals(),
|
|
3096
|
+
services.listThreads({ mode, companyId: props.companyId })
|
|
3097
|
+
]);
|
|
3098
|
+
if (!mounted.current) return;
|
|
3099
|
+
setData({
|
|
3100
|
+
plan: plan.status === "fulfilled" ? plan.value : null,
|
|
3101
|
+
pendingApprovals: approvals.status === "fulfilled" ? approvals.value.length : null,
|
|
3102
|
+
recent: threads.status === "fulfilled" ? threads.value.slice(0, 5).map((t) => ({
|
|
3103
|
+
id: t.id,
|
|
3104
|
+
title: t.title?.trim() || "Untitled chat",
|
|
3105
|
+
when: relativeTime(new Date(t.lastMessageAt ?? t.updatedAt))
|
|
3106
|
+
})) : null,
|
|
3107
|
+
error: threads.status === "rejected" ? toCliError(threads.reason).message : void 0
|
|
3108
|
+
});
|
|
3109
|
+
}, [props.initialData, props.companyId, services, mode]);
|
|
3110
|
+
useEffect8(() => {
|
|
3111
|
+
if (route.name === "home") void loadData();
|
|
3112
|
+
}, [route.name, loadData]);
|
|
3113
|
+
useEffect8(() => {
|
|
3114
|
+
if (!notice) return void 0;
|
|
3115
|
+
const timer = setTimeout(() => setNotice(null), 4e3);
|
|
3116
|
+
return () => clearTimeout(timer);
|
|
3117
|
+
}, [notice]);
|
|
3118
|
+
const commands = useMemo7(() => homeCommands(data, services.cachedToolCount), [data, services.cachedToolCount]);
|
|
3119
|
+
const matches = paletteMatches(commands, editor.value);
|
|
3120
|
+
const selected = Math.min(paletteIndex, Math.max(0, matches.length - 1));
|
|
3121
|
+
const goHome = useCallback6(() => setRoute({ name: "home" }), []);
|
|
3122
|
+
const needSession = useCallback6(
|
|
3123
|
+
(what) => {
|
|
3124
|
+
if (services.account.hasSession) return true;
|
|
3125
|
+
setNotice(theme.attention(`${theme.symbols.attention} ${what} needs a full sign-in. Run \`tabbio login\`.`));
|
|
3126
|
+
return false;
|
|
3127
|
+
},
|
|
3128
|
+
[services.account.hasSession]
|
|
3129
|
+
);
|
|
3130
|
+
const open = useCallback6(
|
|
3131
|
+
(name) => {
|
|
3132
|
+
switch (name) {
|
|
3133
|
+
case "chat":
|
|
3134
|
+
if (needSession("Chat")) setRoute({ name: "chat" });
|
|
3135
|
+
return;
|
|
3136
|
+
case "tools": {
|
|
3137
|
+
if (!services.account.signedIn) {
|
|
3138
|
+
setNotice(theme.attention(`${theme.symbols.attention} Sign in first: run \`tabbio login\`.`));
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
const since = Date.now();
|
|
3142
|
+
setRoute({ name: "tools", since });
|
|
3143
|
+
services.listTools().then((tools) => mounted.current && setRoute((r) => r.name === "tools" ? { ...r, tools } : r)).catch((error) => mounted.current && setRoute((r) => r.name === "tools" ? { ...r, error: toCliError(error).message } : r));
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
case "approvals":
|
|
3147
|
+
if (needSession("Approvals")) setRoute({ name: "approvals" });
|
|
3148
|
+
return;
|
|
3149
|
+
case "status": {
|
|
3150
|
+
setRoute({ name: "status", since: Date.now() });
|
|
3151
|
+
services.loadStatus().then((rows) => mounted.current && setRoute((r) => r.name === "status" ? { ...r, rows } : r)).catch((error) => mounted.current && setRoute((r) => r.name === "status" ? { ...r, error: toCliError(error).message } : r));
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
case "help":
|
|
3155
|
+
setRoute({ name: "help" });
|
|
3156
|
+
return;
|
|
3157
|
+
case "exit":
|
|
3158
|
+
case "quit":
|
|
3159
|
+
onExit();
|
|
3160
|
+
return;
|
|
3161
|
+
default:
|
|
3162
|
+
setNotice(theme.error(`${theme.symbols.error} Unknown command /${name}`));
|
|
3163
|
+
}
|
|
3164
|
+
},
|
|
3165
|
+
[needSession, services, onExit]
|
|
3166
|
+
);
|
|
3167
|
+
const submit2 = useCallback6(
|
|
3168
|
+
(value) => {
|
|
3169
|
+
const text = value.trim();
|
|
3170
|
+
if (!text) return;
|
|
3171
|
+
const slash = parseSlash(text);
|
|
3172
|
+
dispatchEditor({ type: "remember", value: text });
|
|
3173
|
+
dispatchEditor({ type: "clear" });
|
|
3174
|
+
setPaletteIndex(0);
|
|
3175
|
+
if (slash) {
|
|
3176
|
+
const exact = isHomeCommand(slash.name) ? slash.name : void 0;
|
|
3177
|
+
open(exact ?? matches[selected]?.name ?? slash.name);
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
if (needSession("Chat")) setRoute({ name: "chat", initialMessage: text });
|
|
3181
|
+
},
|
|
3182
|
+
[open, matches, selected, needSession]
|
|
3183
|
+
);
|
|
3184
|
+
const onComposerKey = useCallback6(
|
|
3185
|
+
(input, key) => {
|
|
3186
|
+
if (key.ctrl && input === "c") return true;
|
|
3187
|
+
if (matches.length > 0 && (key.upArrow || key.downArrow)) {
|
|
3188
|
+
setPaletteIndex((i) => key.upArrow ? Math.max(0, i - 1) : Math.min(matches.length - 1, i + 1));
|
|
3189
|
+
return true;
|
|
3190
|
+
}
|
|
3191
|
+
if (key.tab) {
|
|
3192
|
+
if (matches.length > 0) {
|
|
3193
|
+
const choice = matches[selected];
|
|
3194
|
+
if (choice) dispatchEditor({ type: "set", value: `/${choice.name}` });
|
|
3195
|
+
} else if (data.recent?.length) setFocus("recent");
|
|
3196
|
+
return true;
|
|
3197
|
+
}
|
|
3198
|
+
if (key.escape) {
|
|
3199
|
+
dispatchEditor({ type: "clear" });
|
|
3200
|
+
return true;
|
|
3201
|
+
}
|
|
3202
|
+
return false;
|
|
3203
|
+
},
|
|
3204
|
+
[matches, selected, data.recent]
|
|
3205
|
+
);
|
|
3206
|
+
useInput10(
|
|
3207
|
+
(input, key) => {
|
|
3208
|
+
if (!(key.ctrl && input === "c")) return;
|
|
3209
|
+
const now = Date.now();
|
|
3210
|
+
if (now - lastCtrlC.current < DOUBLE_PRESS_MS2) {
|
|
3211
|
+
onExit();
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
lastCtrlC.current = now;
|
|
3215
|
+
if (editor.value) dispatchEditor({ type: "clear" });
|
|
3216
|
+
setNotice(theme.attention("Press ctrl+c again to quit."));
|
|
3217
|
+
},
|
|
3218
|
+
{ isActive: route.name !== "chat" }
|
|
3219
|
+
);
|
|
3220
|
+
switch (route.name) {
|
|
3221
|
+
case "chat":
|
|
3222
|
+
return /* @__PURE__ */ jsx25(
|
|
3223
|
+
Chat,
|
|
3224
|
+
{
|
|
3225
|
+
services,
|
|
3226
|
+
mode,
|
|
3227
|
+
companyId: props.companyId,
|
|
3228
|
+
threadId: route.threadId,
|
|
3229
|
+
initialMessage: route.initialMessage,
|
|
3230
|
+
width,
|
|
3231
|
+
height,
|
|
3232
|
+
hostedByHome: true,
|
|
3233
|
+
onExit: (reason) => reason === "home" ? goHome() : onExit()
|
|
3234
|
+
}
|
|
3235
|
+
);
|
|
3236
|
+
case "tools":
|
|
3237
|
+
if (route.tools) return /* @__PURE__ */ jsx25(ToolBrowser, { tools: route.tools, services, width, height, onExit: goHome });
|
|
3238
|
+
return /* @__PURE__ */ jsxs22(Box24, { flexDirection: "column", width, children: [
|
|
3239
|
+
route.error ? /* @__PURE__ */ jsx25(Text24, { children: theme.error(`${theme.symbols.error} ${route.error}`) }) : /* @__PURE__ */ jsx25(Working, { label: "Loading tools\u2026", since: route.since }),
|
|
3240
|
+
/* @__PURE__ */ jsx25(BackKeys, { onBack: goHome })
|
|
3241
|
+
] });
|
|
3242
|
+
case "approvals":
|
|
3243
|
+
return /* @__PURE__ */ jsx25(Approvals, { services, width, height, onExit: goHome });
|
|
3244
|
+
case "status":
|
|
3245
|
+
return /* @__PURE__ */ jsxs22(Box24, { flexDirection: "column", width, children: [
|
|
3246
|
+
/* @__PURE__ */ jsx25(Text24, { children: theme.bold("Status") }),
|
|
3247
|
+
/* @__PURE__ */ jsx25(Box24, { marginTop: 1, flexDirection: "column", children: route.rows ? /* @__PURE__ */ jsx25(KeyValue, { rows: route.rows, width }) : route.error ? /* @__PURE__ */ jsx25(Text24, { children: theme.error(`${theme.symbols.error} ${route.error}`) }) : /* @__PURE__ */ jsx25(Working, { label: "Checking\u2026", since: route.since }) }),
|
|
3248
|
+
/* @__PURE__ */ jsx25(Box24, { marginTop: 1, children: /* @__PURE__ */ jsx25(KeyHints, { width, hints: [{ key: "q", label: "back" }] }) }),
|
|
3249
|
+
/* @__PURE__ */ jsx25(BackKeys, { onBack: goHome })
|
|
3250
|
+
] });
|
|
3251
|
+
case "help":
|
|
3252
|
+
return /* @__PURE__ */ jsxs22(Box24, { flexDirection: "column", width, children: [
|
|
3253
|
+
/* @__PURE__ */ jsx25(Text24, { children: theme.bold("Keys and commands") }),
|
|
3254
|
+
/* @__PURE__ */ jsx25(Box24, { marginTop: 1, children: /* @__PURE__ */ jsx25(KeyValue, { rows: HOME_HELP_ROWS.map(([label, value]) => ({ label, value })), width }) }),
|
|
3255
|
+
/* @__PURE__ */ jsx25(Box24, { marginTop: 1, children: /* @__PURE__ */ jsx25(KeyHints, { width, hints: [{ key: "q", label: "back" }] }) }),
|
|
3256
|
+
/* @__PURE__ */ jsx25(BackKeys, { onBack: goHome })
|
|
3257
|
+
] });
|
|
3258
|
+
default:
|
|
3259
|
+
return /* @__PURE__ */ jsx25(
|
|
3260
|
+
HomeView,
|
|
3261
|
+
{
|
|
3262
|
+
services,
|
|
3263
|
+
data,
|
|
3264
|
+
commands,
|
|
3265
|
+
matches,
|
|
3266
|
+
paletteIndex: selected,
|
|
3267
|
+
editor,
|
|
3268
|
+
dispatchEditor,
|
|
3269
|
+
onSubmit: submit2,
|
|
3270
|
+
onComposerKey,
|
|
3271
|
+
focus,
|
|
3272
|
+
onRecentSelect: (id) => {
|
|
3273
|
+
setFocus("composer");
|
|
3274
|
+
setRoute({ name: "chat", threadId: id });
|
|
3275
|
+
},
|
|
3276
|
+
onRecentCancel: () => setFocus("composer"),
|
|
3277
|
+
notice,
|
|
3278
|
+
width,
|
|
3279
|
+
height
|
|
3280
|
+
}
|
|
3281
|
+
);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
// src/ui/services.ts
|
|
3286
|
+
function hostOf2(url) {
|
|
3287
|
+
try {
|
|
3288
|
+
return new URL(url).host;
|
|
3289
|
+
} catch {
|
|
3290
|
+
return url;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
function createUiServices(ctx) {
|
|
3294
|
+
const creds = ctx.api.credentials ?? ctx.credentials;
|
|
3295
|
+
const cached = readCachedCatalog(ctx.profile, creds);
|
|
3296
|
+
return {
|
|
3297
|
+
version: CLI_VERSION,
|
|
3298
|
+
profileName: ctx.profile.name,
|
|
3299
|
+
apiHost: hostOf2(ctx.profile.apiUrl),
|
|
3300
|
+
account: {
|
|
3301
|
+
name: creds?.user?.name ?? null,
|
|
3302
|
+
email: creds?.user?.email ?? null,
|
|
3303
|
+
signedIn: Boolean(creds?.accessToken || creds?.mcpToken),
|
|
3304
|
+
hasSession: Boolean(creds?.accessToken),
|
|
3305
|
+
userId: creds?.user?.id
|
|
3306
|
+
},
|
|
3307
|
+
cachedToolCount: cached ? cached.tools.filter((t) => !t.hidden).length : null,
|
|
3308
|
+
async loadPlan() {
|
|
3309
|
+
if (!creds?.accessToken) return null;
|
|
3310
|
+
const state = await ctx.api.json("/api/subscription/state");
|
|
3311
|
+
return state?.snapshot?.tier ?? null;
|
|
3312
|
+
},
|
|
3313
|
+
async loadPendingApprovals() {
|
|
3314
|
+
const state = await getMcpAccessState(ctx.api);
|
|
3315
|
+
return state?.pendingApprovals ?? [];
|
|
3316
|
+
},
|
|
3317
|
+
listThreads: (scope) => listThreads(ctx, scope),
|
|
3318
|
+
listModels: (scope) => listModels(ctx, scope),
|
|
3319
|
+
getThreadMessages: (threadId, scope) => getThreadMessages(ctx, threadId, scope),
|
|
3320
|
+
streamChat: (request, signal) => streamChat(ctx, request, { signal }),
|
|
3321
|
+
decideApproval: (approvalId, decision, scope) => decision === "approve" ? confirmApproval(ctx, approvalId, scope) : rejectApproval(ctx, approvalId, scope),
|
|
3322
|
+
async listTools(opts) {
|
|
3323
|
+
const session = await ctx.mcp();
|
|
3324
|
+
return session.listTools(opts);
|
|
3325
|
+
},
|
|
3326
|
+
async callTool(toolId, input, opts) {
|
|
3327
|
+
const session = await ctx.mcp();
|
|
3328
|
+
const started = Date.now();
|
|
3329
|
+
const result = await session.callTool(toolId, input, opts);
|
|
3330
|
+
return { ...result, durationMs: Date.now() - started };
|
|
3331
|
+
},
|
|
3332
|
+
capabilityPrefs: loadCapabilityPrefs(ctx.profile.name),
|
|
3333
|
+
saveCapabilityPrefs: (capabilities) => saveCapabilityPrefs(ctx.profile.name, capabilities),
|
|
3334
|
+
loadCapabilities: () => ctx.api.json("/api/agent/capabilities"),
|
|
3335
|
+
getArtifact: (artifactId) => getArtifact(ctx.api, artifactId),
|
|
3336
|
+
async artifactPreviewUrl(artifactId, kind) {
|
|
3337
|
+
return (await previewUrl(ctx.api, artifactId, { variant: previewVariantFor(kind) })).url;
|
|
3338
|
+
},
|
|
3339
|
+
hyperlinks: supportsHyperlinks(),
|
|
3340
|
+
async loadStatus() {
|
|
3341
|
+
const { collectStatus } = await import("./status-I5UAQPBY.js");
|
|
3342
|
+
const report = await collectStatus(ctx);
|
|
3343
|
+
const who = report.user ? report.user.name ? `${report.user.name} <${report.user.email}>` : report.user.email : "not signed in";
|
|
3344
|
+
const rows = [
|
|
3345
|
+
["Profile", `${report.profile.name} (${report.profile.source})`],
|
|
3346
|
+
["API", report.profile.apiUrl],
|
|
3347
|
+
["Account", who],
|
|
3348
|
+
["Plan", report.plan],
|
|
3349
|
+
["Session", report.session ? `${report.session.token}${report.session.refreshable ? " \xB7 auto-refresh" : ""}` : null],
|
|
3350
|
+
["Tools via", report.mcp?.label],
|
|
3351
|
+
["Approvals", report.pendingApprovals === null ? null : `${report.pendingApprovals} pending`],
|
|
3352
|
+
["Catalog", report.catalog ? `${report.catalog.tools} tools${report.catalog.fresh ? "" : " (stale)"}` : null],
|
|
3353
|
+
...report.problems.map((p) => ["Problem", p])
|
|
3354
|
+
];
|
|
3355
|
+
return rows.filter((r) => Boolean(r[1])).map(([label, value]) => ({ label, value }));
|
|
3356
|
+
}
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
// src/ui/entry.tsx
|
|
3361
|
+
import { jsx as jsx26, jsxs as jsxs23 } from "react/jsx-runtime";
|
|
3362
|
+
function unwrap(outcome) {
|
|
3363
|
+
if (outcome.status === "interrupted") throw interruptedError();
|
|
3364
|
+
return outcome.value;
|
|
3365
|
+
}
|
|
3366
|
+
async function runBrowseTools(ctx, tools, options) {
|
|
3367
|
+
const services = createUiServices(ctx);
|
|
3368
|
+
unwrap(
|
|
3369
|
+
await renderScreen("The tool browser", ({ finish, width, height }) => /* @__PURE__ */ jsx26(
|
|
3370
|
+
ToolBrowser,
|
|
3371
|
+
{
|
|
3372
|
+
tools,
|
|
3373
|
+
services,
|
|
3374
|
+
width,
|
|
3375
|
+
height,
|
|
3376
|
+
initialQuery: options.query,
|
|
3377
|
+
initialGroup: options.group,
|
|
3378
|
+
onExit: () => finish(void 0)
|
|
3379
|
+
}
|
|
3380
|
+
))
|
|
3381
|
+
);
|
|
3382
|
+
}
|
|
3383
|
+
async function runToolForm(_ctx, tool, initial, options) {
|
|
3384
|
+
const skip = autoFilledProperties(tool, tool.inputSchema);
|
|
3385
|
+
const state = createFormState(tool.inputSchema, initial, { skip, missingRequired: options.missingRequired });
|
|
3386
|
+
return unwrap(
|
|
3387
|
+
await renderScreen("The input form", ({ finish, width, height }) => /* @__PURE__ */ jsxs23(Box25, { flexDirection: "column", width, children: [
|
|
3388
|
+
/* @__PURE__ */ jsxs23(Box25, { flexDirection: "column", marginBottom: 1, children: [
|
|
3389
|
+
/* @__PURE__ */ jsxs23(Text25, { wrap: "truncate-end", children: [
|
|
3390
|
+
theme.bold(toolLabel(tool)),
|
|
3391
|
+
" ",
|
|
3392
|
+
theme.dim(`${theme.symbols.middot} ${oneLine(tool.title)}`)
|
|
3393
|
+
] }),
|
|
3394
|
+
options.missingRequired.length ? /* @__PURE__ */ jsx26(Text25, { wrap: "truncate-end", children: theme.dim(`Missing: ${options.missingRequired.join(", ")}`) }) : null
|
|
3395
|
+
] }),
|
|
3396
|
+
/* @__PURE__ */ jsx26(
|
|
3397
|
+
Form,
|
|
3398
|
+
{
|
|
3399
|
+
initial: state,
|
|
3400
|
+
width,
|
|
3401
|
+
height: Math.max(3, height - 5),
|
|
3402
|
+
isActive: true,
|
|
3403
|
+
onSubmit: (input) => finish(input, /* @__PURE__ */ jsx26(Text25, { wrap: "truncate-end", children: theme.dim(`${theme.symbols.pointer} ${equivalentCommand(tool, input, skip)}`) })),
|
|
3404
|
+
onCancel: () => finish(null)
|
|
3405
|
+
}
|
|
3406
|
+
)
|
|
3407
|
+
] }))
|
|
3408
|
+
);
|
|
3409
|
+
}
|
|
3410
|
+
async function runApprovalPrompt(ctx, approval) {
|
|
3411
|
+
const shownAt = Date.now();
|
|
3412
|
+
const title = sentenceCase(approval.toolName || "this action");
|
|
3413
|
+
return unwrap(
|
|
3414
|
+
await renderScreen("The approval prompt", ({ finish, width }) => /* @__PURE__ */ jsx26(
|
|
3415
|
+
ApprovalCard,
|
|
3416
|
+
{
|
|
3417
|
+
approvalId: approval.approvalId,
|
|
3418
|
+
title,
|
|
3419
|
+
message: approval.message,
|
|
3420
|
+
args: approval.args,
|
|
3421
|
+
destructive: isDestructiveTool(approval.toolName),
|
|
3422
|
+
context: `Writes to your account ${theme.symbols.middot} profile ${ctx.profile.name}`,
|
|
3423
|
+
isActive: true,
|
|
3424
|
+
width,
|
|
3425
|
+
onDecision: (decision) => finish(decision, /* @__PURE__ */ jsx26(Text25, { children: approvalOutcomeText(decision, title, approval.approvalId, Date.now() - shownAt) }))
|
|
3426
|
+
}
|
|
3427
|
+
))
|
|
3428
|
+
);
|
|
3429
|
+
}
|
|
3430
|
+
function PrintOnce({ children, onPrinted }) {
|
|
3431
|
+
const { columns } = useTerminalSize();
|
|
3432
|
+
useEffect9(() => {
|
|
3433
|
+
onPrinted();
|
|
3434
|
+
}, [onPrinted]);
|
|
3435
|
+
return /* @__PURE__ */ jsx26(Static2, { items: ["once"], children: (key) => /* @__PURE__ */ jsx26(Page, { columns, children }, key) });
|
|
3436
|
+
}
|
|
3437
|
+
async function runShowResult(_ctx, tool, result, meta) {
|
|
3438
|
+
unwrap(
|
|
3439
|
+
await renderScreen("The result panel", ({ finish, width, height }) => /* @__PURE__ */ jsx26(PrintOnce, { onPrinted: () => finish(void 0), children: /* @__PURE__ */ jsx26(
|
|
3440
|
+
ResultPanel,
|
|
3441
|
+
{
|
|
3442
|
+
title: toolLabel(tool),
|
|
3443
|
+
result,
|
|
3444
|
+
durationMs: meta.durationMs,
|
|
3445
|
+
requestId: meta.requestId,
|
|
3446
|
+
width,
|
|
3447
|
+
height,
|
|
3448
|
+
interactive: false
|
|
3449
|
+
}
|
|
3450
|
+
) }))
|
|
3451
|
+
);
|
|
3452
|
+
}
|
|
3453
|
+
async function runHome(ctx, opts = {}) {
|
|
3454
|
+
const services = createUiServices(ctx);
|
|
3455
|
+
await renderScreen(
|
|
3456
|
+
"The home screen",
|
|
3457
|
+
({ finish, width, height }) => /* @__PURE__ */ jsx26(Home, { services, width, height, mode: opts.mode, companyId: opts.companyId, onExit: () => finish(void 0) }),
|
|
3458
|
+
{ ctrlC: "screen" }
|
|
3459
|
+
);
|
|
3460
|
+
}
|
|
3461
|
+
async function runChat(ctx, opts) {
|
|
3462
|
+
const services = createUiServices(ctx);
|
|
3463
|
+
await renderScreen(
|
|
3464
|
+
"Chat",
|
|
3465
|
+
({ finish, width, height }) => /* @__PURE__ */ jsx26(
|
|
3466
|
+
Chat,
|
|
3467
|
+
{
|
|
3468
|
+
services,
|
|
3469
|
+
mode: opts.mode,
|
|
3470
|
+
companyId: opts.companyId,
|
|
3471
|
+
threadId: opts.threadId,
|
|
3472
|
+
modelId: opts.modelId,
|
|
3473
|
+
initialMessage: opts.message,
|
|
3474
|
+
capabilities: opts.capabilities,
|
|
3475
|
+
width,
|
|
3476
|
+
height,
|
|
3477
|
+
onExit: () => finish(void 0)
|
|
3478
|
+
}
|
|
3479
|
+
),
|
|
3480
|
+
{ ctrlC: "screen" }
|
|
3481
|
+
);
|
|
3482
|
+
}
|
|
3483
|
+
export {
|
|
3484
|
+
PrintOnce,
|
|
3485
|
+
runApprovalPrompt,
|
|
3486
|
+
runBrowseTools,
|
|
3487
|
+
runChat,
|
|
3488
|
+
runHome,
|
|
3489
|
+
runShowResult,
|
|
3490
|
+
runToolForm
|
|
3491
|
+
};
|
|
3492
|
+
//# sourceMappingURL=entry-WENOOT6W.js.map
|