@matterailab/orbcode 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/App.js +166 -44
- package/package.json +1 -1
package/dist/ui/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
|
-
import { Box,
|
|
3
|
+
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
4
4
|
import open from "open";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { COLORS, VERSION } from "../branding.js";
|
|
@@ -226,6 +226,9 @@ function rowId() {
|
|
|
226
226
|
}
|
|
227
227
|
export function App({ initialView, initialPrompt, initialSession, systemPromptOverride, updateCheck, }) {
|
|
228
228
|
const { exit } = useApp();
|
|
229
|
+
const { stdout } = useStdout();
|
|
230
|
+
const termRows = stdout?.rows ?? 24;
|
|
231
|
+
const termCols = stdout?.columns ?? 80;
|
|
229
232
|
const [settings, setSettings] = useState(() => loadSettings());
|
|
230
233
|
const [view, setView] = useState(initialView ?? (getAuthToken(settings) ? "chat" : "login"));
|
|
231
234
|
const [updateInfo, setUpdateInfo] = useState(null);
|
|
@@ -266,7 +269,6 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
266
269
|
const [mcpMigrationEntries, setMcpMigrationEntries] = useState(null);
|
|
267
270
|
// Project-scope MCP servers awaiting the user's approval at startup.
|
|
268
271
|
const [pendingMcpApproval, setPendingMcpApproval] = useState(null);
|
|
269
|
-
const [staticKey, setStaticKey] = useState(0);
|
|
270
272
|
const [tasks, setTasks] = useState("");
|
|
271
273
|
const [contextTokens, setContextTokens] = useState(0);
|
|
272
274
|
const [totalCost, setTotalCost] = useState(0);
|
|
@@ -356,7 +358,6 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
356
358
|
modelName: getModel(loadSettings().model).name,
|
|
357
359
|
},
|
|
358
360
|
]);
|
|
359
|
-
setStaticKey((k) => k + 1);
|
|
360
361
|
}, []);
|
|
361
362
|
const handleEvent = useCallback((event) => {
|
|
362
363
|
switch (event.type) {
|
|
@@ -1068,7 +1069,6 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
1068
1069
|
if (process.stdout.isTTY) {
|
|
1069
1070
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
1070
1071
|
}
|
|
1071
|
-
setStaticKey((k) => k + 1);
|
|
1072
1072
|
}
|
|
1073
1073
|
});
|
|
1074
1074
|
const handleLogin = useCallback((token, profile) => {
|
|
@@ -1103,48 +1103,170 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
1103
1103
|
!resumableSessions &&
|
|
1104
1104
|
!taskPickerSessions &&
|
|
1105
1105
|
!linkManagerOpen;
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1106
|
+
// ── Viewport management ─────────────────────────────────────────────
|
|
1107
|
+
// Instead of <Static> (which permanently writes to stdout and causes
|
|
1108
|
+
// uncontrolled scrolling), we render everything in the dynamic region and
|
|
1109
|
+
// only show the rows that fit above the pinned input box + status bar.
|
|
1110
|
+
// This keeps the input box always at the bottom of the terminal.
|
|
1111
|
+
// Height of the fixed bottom chrome (input box border + text + status bar +
|
|
1112
|
+
// margins). Intentionally slightly overestimated so we never push the
|
|
1113
|
+
// input box off-screen.
|
|
1114
|
+
let bottomHeight = 8;
|
|
1115
|
+
if (queuedMessages.length > 0)
|
|
1116
|
+
bottomHeight += 2 + Math.min(5, queuedMessages.length);
|
|
1117
|
+
if (pendingApproval ||
|
|
1118
|
+
pendingFollowup ||
|
|
1119
|
+
pendingHookTrust ||
|
|
1120
|
+
pendingMcpApproval ||
|
|
1121
|
+
modelPickerOpen ||
|
|
1122
|
+
mcpPickerOpen ||
|
|
1123
|
+
mcpMigrationEntries ||
|
|
1124
|
+
resumableSessions ||
|
|
1125
|
+
taskPickerSessions ||
|
|
1126
|
+
linkManagerOpen)
|
|
1127
|
+
bottomHeight += 10;
|
|
1128
|
+
// Height of the middle dynamic elements (between rows and input box).
|
|
1129
|
+
let middleHeight = 0;
|
|
1130
|
+
if (streamingReasoning)
|
|
1131
|
+
middleHeight += 4;
|
|
1132
|
+
if (taskLines.length > 0)
|
|
1133
|
+
middleHeight += 1 + Math.min(10, taskLines.length);
|
|
1134
|
+
if (busy && !streamingText && !streamingReasoning)
|
|
1135
|
+
middleHeight += 1;
|
|
1136
|
+
if (updateInfo?.updateAvailable && updateInfo.latest)
|
|
1137
|
+
middleHeight += 4;
|
|
1138
|
+
// Available height for committed rows + streaming text.
|
|
1139
|
+
const availableHeight = Math.max(3, termRows - bottomHeight - middleHeight);
|
|
1140
|
+
// Estimate how many terminal lines a row will occupy.
|
|
1141
|
+
const wrapWidth = Math.max(20, termCols - 4);
|
|
1142
|
+
const visibleRows = useMemo(() => {
|
|
1143
|
+
let used = 0;
|
|
1144
|
+
let start = rows.length;
|
|
1145
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
1146
|
+
const h = estimateRowLines(rows[i], wrapWidth);
|
|
1147
|
+
if (used + h > availableHeight)
|
|
1148
|
+
break;
|
|
1149
|
+
used += h;
|
|
1150
|
+
start = i;
|
|
1151
|
+
}
|
|
1152
|
+
return rows.slice(start);
|
|
1153
|
+
}, [rows, availableHeight, wrapWidth]);
|
|
1154
|
+
// Cap streaming text to the space left after committed rows.
|
|
1155
|
+
const rowsHeight = useMemo(() => visibleRows.reduce((s, r) => s + estimateRowLines(r, wrapWidth), 0), [visibleRows, wrapWidth]);
|
|
1156
|
+
const streamingMaxLines = Math.max(3, availableHeight - rowsHeight);
|
|
1157
|
+
const streamingDisplay = streamingText
|
|
1158
|
+
? tailForHeight(streamingText, streamingMaxLines, wrapWidth)
|
|
1159
|
+
: "";
|
|
1160
|
+
const streamingReasoningDisplay = streamingReasoning
|
|
1161
|
+
? tailForHeight(streamingReasoning, 3, wrapWidth)
|
|
1162
|
+
: "";
|
|
1163
|
+
// Spacer to push the input box to the bottom when content is short.
|
|
1164
|
+
const streamingHeight = streamingDisplay
|
|
1165
|
+
? Math.min(streamingMaxLines, wrapHeight(streamingDisplay, wrapWidth))
|
|
1166
|
+
: 0;
|
|
1167
|
+
const spacerHeight = Math.max(0, availableHeight - rowsHeight - streamingHeight);
|
|
1168
|
+
return (_jsx(Box, { flexDirection: "column", children: view === "login" ? (_jsx(LoginSection, { onLogin: handleLogin })) : (_jsxs(Box, { flexDirection: "column", children: [visibleRows.map((row) => (_jsx(RowView, { row: row }, row.id))), updateInfo?.updateAvailable && updateInfo.latest && (_jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: COLORS.warning, paddingX: 2, alignSelf: "flex-start", children: [_jsxs(Text, { color: COLORS.warning, bold: true, children: ["\u2191 Update available: v", updateInfo.current, " \u2192 v", updateInfo.latest] }), _jsxs(Text, { children: ["Run ", _jsx(Text, { color: COLORS.accent, children: "orbcode update" }), " to install the latest version, then relaunch."] })] })), streamingReasoning && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: COLORS.thinking, italic: true, children: "\u2726 Thinking\u2026" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, italic: true, children: streamingReasoningDisplay }) })] })), streamingDisplay && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: COLORS.primary, children: "\u25CF " }), streamingDisplay] }) })), spacerHeight > 0 && _jsx(Box, { height: spacerHeight }), taskLines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: "Tasks" }), taskLines.slice(0, 10).map((line, i) => (_jsx(Text, { dimColor: /^[-*]\s*\[x\]/i.test(line), children: line
|
|
1169
|
+
.replace(/^[-*]\s*\[x\]/i, " ■")
|
|
1170
|
+
.replace(/^[-*]\s*\[-\]/, " ◧")
|
|
1171
|
+
.replace(/^[-*]\s*\[ \]/, " □") }, i))), taskLines.length > 10 && (_jsxs(Text, { dimColor: true, children: [" \u2026 ", taskLines.length - 10, " more"] }))] })), modelPickerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(ModelPicker, { currentId: settings.model, onSelect: (modelId) => {
|
|
1172
|
+
setModelPickerOpen(false);
|
|
1173
|
+
switchModel(modelId);
|
|
1174
|
+
}, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), taskPickerSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: taskPickerSessions, title: "Reference a previous task", onSelect: handleTaskSelect, onCancel: () => setTaskPickerSessions(null) }) })), linkManagerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(LinkManager, { links: links, status: linkStatus, onAdd: (input) => {
|
|
1175
|
+
const result = addLink(process.cwd(), input);
|
|
1176
|
+
setLinkStatus(result.message);
|
|
1177
|
+
if (result.ok)
|
|
1119
1178
|
setLinks(loadLinks(process.cwd()));
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1179
|
+
}, onRemove: (link) => {
|
|
1180
|
+
removeLink(process.cwd(), link.path);
|
|
1181
|
+
setLinks(loadLinks(process.cwd()));
|
|
1182
|
+
setLinkStatus(`Unlinked ${path.basename(link.path)}`);
|
|
1183
|
+
}, onClose: () => setLinkManagerOpen(false) }) })), pendingHookTrust && (_jsx(Box, { marginTop: 1, children: _jsx(HookTrustPrompt, { cwd: process.cwd(), commands: pendingHookTrust.commands, onDecision: resolveHookTrust }) })), pendingMcpApproval && (_jsx(Box, { marginTop: 1, children: _jsx(McpApprovalPrompt, { serverNames: pendingMcpApproval, onApprove: resolveMcpApproval }) })), mcpPickerOpen && mcpManagerRef.current && (_jsx(Box, { marginTop: 1, children: _jsx(McpPicker, { manager: mcpManagerRef.current, onChanged: () => {
|
|
1184
|
+
const m = mcpManagerRef.current;
|
|
1185
|
+
if (m)
|
|
1186
|
+
saveMcpApproval(process.cwd(), m.getEnabled(), m.getDisabled());
|
|
1187
|
+
}, onCancel: () => setMcpPickerOpen(false), onDeleted: handleMcpServerDeleted }) })), mcpMigrationEntries && (_jsx(Box, { marginTop: 1, children: _jsx(McpMigrationPicker, { entries: mcpMigrationEntries, onCancel: () => setMcpMigrationEntries(null), onConfirm: (selected) => {
|
|
1188
|
+
setMcpMigrationEntries(null);
|
|
1189
|
+
resolveMcpMigration(selected);
|
|
1190
|
+
} }) })), pendingApproval && (_jsx(Box, { marginTop: 1, children: _jsx(ApprovalPrompt, { request: pendingApproval.request, onDecision: (decision) => {
|
|
1191
|
+
pendingApproval.resolve(decision);
|
|
1192
|
+
setPendingApproval(null);
|
|
1193
|
+
} }) })), pendingFollowup && (_jsx(Box, { marginTop: 1, children: _jsx(FollowupPrompt, { question: pendingFollowup.question, suggestions: pendingFollowup.suggestions, onAnswer: (answer) => {
|
|
1194
|
+
pushRow({ kind: "user", text: answer });
|
|
1195
|
+
pendingFollowup.resolve(answer);
|
|
1196
|
+
setPendingFollowup(null);
|
|
1197
|
+
} }) })), busy &&
|
|
1198
|
+
!pendingApproval &&
|
|
1199
|
+
!pendingFollowup &&
|
|
1200
|
+
!pendingHookTrust &&
|
|
1201
|
+
!pendingMcpApproval &&
|
|
1202
|
+
!mcpPickerOpen &&
|
|
1203
|
+
!mcpMigrationEntries &&
|
|
1204
|
+
!streamingText &&
|
|
1205
|
+
!streamingReasoning && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: busyLabel }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [queuedMessages.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginBottom: 1, children: [_jsxs(Text, { dimColor: true, bold: true, children: ["Queue (", queuedMessages.length, ")"] }), queuedMessages.slice(0, 5).map((msg, i) => (_jsxs(Text, { dimColor: true, children: [i + 1, ". ", truncateForQueue(msg).replace(/\n/g, "↵")] }, i))), queuedMessages.length > 5 && (_jsxs(Text, { dimColor: true, children: [" \u2026 ", queuedMessages.length - 5, " more"] }))] })), _jsx(InputBox, { active: inputActive, slashCommands: SLASH_COMMANDS, onSubmit: handleSubmit }), _jsx(StatusBar, { modelId: settings.model, contextTokens: contextTokens, totalCost: totalCost, state: busy ? busyLabel : "", approvalMode: approvalMode, busy: busy, title: sessionTitle, plan: usage?.plan, usagePercentage: usage?.usagePercentage, tieredUsage: usage?.tieredUsage })] })] })) }));
|
|
1206
|
+
}
|
|
1207
|
+
/** Count the number of terminal rows a block of text will occupy (accounting
|
|
1208
|
+
* for wrapping at `width` columns). */
|
|
1209
|
+
function wrapHeight(text, width) {
|
|
1210
|
+
return text.split("\n").reduce((sum, line) => sum + Math.max(1, Math.ceil(Math.max(1, line.length) / width)), 0);
|
|
1211
|
+
}
|
|
1212
|
+
/** Return the tail of `text` that fits within `maxLines` terminal rows,
|
|
1213
|
+
* accounting for line wrapping at `width` columns. */
|
|
1214
|
+
function tailForHeight(text, maxLines, width) {
|
|
1215
|
+
const lines = text.split("\n");
|
|
1216
|
+
const result = [];
|
|
1217
|
+
let height = 0;
|
|
1218
|
+
for (let i = lines.length - 1; i >= 0 && height < maxLines; i--) {
|
|
1219
|
+
const line = lines[i];
|
|
1220
|
+
const wrapped = Math.max(1, Math.ceil(Math.max(1, line.length) / width));
|
|
1221
|
+
if (height + wrapped > maxLines) {
|
|
1222
|
+
const remaining = maxLines - height;
|
|
1223
|
+
if (remaining > 0) {
|
|
1224
|
+
const chars = remaining * width;
|
|
1225
|
+
result.unshift(line.slice(-chars));
|
|
1226
|
+
}
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
result.unshift(line);
|
|
1230
|
+
height += wrapped;
|
|
1231
|
+
}
|
|
1232
|
+
return result.join("\n");
|
|
1144
1233
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1234
|
+
/** Estimate how many terminal lines a committed row will occupy. Intentionally
|
|
1235
|
+
* slightly conservative (overestimates) so we never show too many rows and
|
|
1236
|
+
* push the input box off-screen. */
|
|
1237
|
+
function estimateRowLines(row, width) {
|
|
1238
|
+
const w = Math.max(20, width);
|
|
1239
|
+
function wrapped(text) {
|
|
1240
|
+
return text.split("\n").reduce((sum, line) => sum + Math.max(1, Math.ceil(Math.max(1, line.length) / w)), 0);
|
|
1241
|
+
}
|
|
1242
|
+
switch (row.kind) {
|
|
1243
|
+
case "header":
|
|
1244
|
+
return 9;
|
|
1245
|
+
case "user":
|
|
1246
|
+
return 1 + wrapped(row.text);
|
|
1247
|
+
case "assistant":
|
|
1248
|
+
return 1 + wrapped(row.text);
|
|
1249
|
+
case "reasoning":
|
|
1250
|
+
return row.expanded ? 1 + wrapped(row.text) : 1;
|
|
1251
|
+
case "tool": {
|
|
1252
|
+
let h = 1;
|
|
1253
|
+
if (row.diff) {
|
|
1254
|
+
h += Math.min(62, row.diff.split("\n").length) + 1;
|
|
1255
|
+
}
|
|
1256
|
+
else if (row.resultPreview) {
|
|
1257
|
+
h += wrapped(row.resultPreview);
|
|
1258
|
+
}
|
|
1259
|
+
return h;
|
|
1260
|
+
}
|
|
1261
|
+
case "info":
|
|
1262
|
+
return Math.max(1, wrapped(row.text));
|
|
1263
|
+
case "error":
|
|
1264
|
+
return Math.max(1, wrapped(row.text));
|
|
1265
|
+
case "completion":
|
|
1266
|
+
return 2 + wrapped(row.text);
|
|
1267
|
+
default:
|
|
1268
|
+
return 1;
|
|
1269
|
+
}
|
|
1148
1270
|
}
|
|
1149
1271
|
const QUEUE_PREVIEW_LIMIT = 80;
|
|
1150
1272
|
function truncateForQueue(text) {
|