reasonix 0.12.13 → 0.12.15
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/dashboard/app.css +71 -0
- package/dashboard/app.js +188 -20
- package/dist/cli/index.js +196 -52
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dashboard/app.css
CHANGED
|
@@ -1300,6 +1300,13 @@ textarea:focus {
|
|
|
1300
1300
|
.chat-inflight-sep {
|
|
1301
1301
|
color: var(--fg-3);
|
|
1302
1302
|
}
|
|
1303
|
+
.chat-inflight-tool {
|
|
1304
|
+
color: var(--fg-1);
|
|
1305
|
+
white-space: nowrap;
|
|
1306
|
+
overflow: hidden;
|
|
1307
|
+
text-overflow: ellipsis;
|
|
1308
|
+
max-width: 480px;
|
|
1309
|
+
}
|
|
1303
1310
|
.chat-inflight-abort {
|
|
1304
1311
|
margin-left: auto;
|
|
1305
1312
|
background: transparent;
|
|
@@ -1475,6 +1482,11 @@ textarea:focus {
|
|
|
1475
1482
|
padding: 8px 12px;
|
|
1476
1483
|
font-family: var(--mono);
|
|
1477
1484
|
font-size: 13px;
|
|
1485
|
+
overflow-x: auto;
|
|
1486
|
+
max-height: 240px;
|
|
1487
|
+
overflow-y: auto;
|
|
1488
|
+
white-space: pre-wrap;
|
|
1489
|
+
word-break: break-all;
|
|
1478
1490
|
}
|
|
1479
1491
|
.modal-cmd-prompt {
|
|
1480
1492
|
color: var(--fg-3);
|
|
@@ -1484,6 +1496,7 @@ textarea:focus {
|
|
|
1484
1496
|
color: var(--primary);
|
|
1485
1497
|
background: transparent;
|
|
1486
1498
|
padding: 0;
|
|
1499
|
+
word-break: break-all;
|
|
1487
1500
|
}
|
|
1488
1501
|
|
|
1489
1502
|
.modal-actions {
|
|
@@ -1554,6 +1567,64 @@ textarea:focus {
|
|
|
1554
1567
|
overflow-y: auto;
|
|
1555
1568
|
}
|
|
1556
1569
|
|
|
1570
|
+
/* Plan-revision modal — list of remaining steps with risk dots. */
|
|
1571
|
+
.modal-revise-reason {
|
|
1572
|
+
background: var(--bg-2);
|
|
1573
|
+
border-left: 3px solid #c4b5fd;
|
|
1574
|
+
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
1575
|
+
padding: 8px 12px;
|
|
1576
|
+
color: var(--fg-1);
|
|
1577
|
+
font-size: 13px;
|
|
1578
|
+
white-space: pre-wrap;
|
|
1579
|
+
}
|
|
1580
|
+
.modal-revise-steps {
|
|
1581
|
+
list-style: none;
|
|
1582
|
+
margin: 0;
|
|
1583
|
+
padding: 0;
|
|
1584
|
+
display: flex;
|
|
1585
|
+
flex-direction: column;
|
|
1586
|
+
gap: 6px;
|
|
1587
|
+
max-height: 280px;
|
|
1588
|
+
overflow-y: auto;
|
|
1589
|
+
}
|
|
1590
|
+
.modal-revise-steps li {
|
|
1591
|
+
display: grid;
|
|
1592
|
+
grid-template-columns: 12px 64px 1fr;
|
|
1593
|
+
grid-template-rows: auto auto;
|
|
1594
|
+
gap: 2px 10px;
|
|
1595
|
+
background: var(--bg-2);
|
|
1596
|
+
border: 1px solid var(--border);
|
|
1597
|
+
border-radius: var(--radius-sm);
|
|
1598
|
+
padding: 8px 12px;
|
|
1599
|
+
align-items: center;
|
|
1600
|
+
}
|
|
1601
|
+
.modal-revise-dot {
|
|
1602
|
+
grid-row: 1 / 3;
|
|
1603
|
+
width: 8px;
|
|
1604
|
+
height: 8px;
|
|
1605
|
+
border-radius: 50%;
|
|
1606
|
+
align-self: center;
|
|
1607
|
+
}
|
|
1608
|
+
.modal-revise-id {
|
|
1609
|
+
grid-row: 1;
|
|
1610
|
+
font-family: var(--mono);
|
|
1611
|
+
font-size: 12px;
|
|
1612
|
+
color: var(--fg-2);
|
|
1613
|
+
}
|
|
1614
|
+
.modal-revise-title {
|
|
1615
|
+
grid-row: 1;
|
|
1616
|
+
color: var(--fg-0);
|
|
1617
|
+
font-size: 13px;
|
|
1618
|
+
font-weight: 500;
|
|
1619
|
+
}
|
|
1620
|
+
.modal-revise-action {
|
|
1621
|
+
grid-column: 2 / 4;
|
|
1622
|
+
grid-row: 2;
|
|
1623
|
+
color: var(--fg-2);
|
|
1624
|
+
font-size: 12px;
|
|
1625
|
+
white-space: pre-wrap;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1557
1628
|
.modal-edit-preview {
|
|
1558
1629
|
background: var(--bg-2);
|
|
1559
1630
|
border: 1px solid var(--border);
|
package/dashboard/app.js
CHANGED
|
@@ -1136,9 +1136,103 @@ function EditReviewModal({ modal, onResolve }) {
|
|
|
1136
1136
|
`;
|
|
1137
1137
|
}
|
|
1138
1138
|
|
|
1139
|
+
function WorkspaceModal({ modal, onResolve }) {
|
|
1140
|
+
return html`
|
|
1141
|
+
<${ModalCard}
|
|
1142
|
+
accent="#fbbf24"
|
|
1143
|
+
icon="◇"
|
|
1144
|
+
title="model wants to switch workspace"
|
|
1145
|
+
subtitle="every subsequent file / shell / memory tool resolves against the new root"
|
|
1146
|
+
>
|
|
1147
|
+
<div class="modal-cmd"><span class="modal-cmd-prompt">→</span> <code>${modal.path}</code></div>
|
|
1148
|
+
<div class="modal-actions">
|
|
1149
|
+
<button class="primary" onClick=${() => onResolve("workspace", "switch")}>Switch (Enter)</button>
|
|
1150
|
+
<button class="danger" onClick=${() => onResolve("workspace", "deny")}>Deny (Esc)</button>
|
|
1151
|
+
</div>
|
|
1152
|
+
<//>
|
|
1153
|
+
`;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function CheckpointModal({ modal, onResolve }) {
|
|
1157
|
+
const [reviseText, setReviseText] = useState("");
|
|
1158
|
+
const [staged, setStaged] = useState(false);
|
|
1159
|
+
const label = modal.title ? `${modal.stepId} · ${modal.title}` : modal.stepId;
|
|
1160
|
+
const counter = modal.total > 0 ? ` (${modal.completed}/${modal.total})` : "";
|
|
1161
|
+
return html`
|
|
1162
|
+
<${ModalCard}
|
|
1163
|
+
accent="#a5f3fc"
|
|
1164
|
+
icon="✓"
|
|
1165
|
+
title=${`step complete${counter}`}
|
|
1166
|
+
subtitle=${label}
|
|
1167
|
+
>
|
|
1168
|
+
${
|
|
1169
|
+
staged
|
|
1170
|
+
? html`
|
|
1171
|
+
<textarea
|
|
1172
|
+
placeholder="What needs to change before the next step? Leave blank to just continue."
|
|
1173
|
+
rows="3"
|
|
1174
|
+
value=${reviseText}
|
|
1175
|
+
onInput=${(e) => setReviseText(e.target.value)}
|
|
1176
|
+
></textarea>
|
|
1177
|
+
<div class="modal-actions">
|
|
1178
|
+
<button class="primary" onClick=${() => onResolve("checkpoint", "revise", reviseText)}>Send revision</button>
|
|
1179
|
+
<button onClick=${() => {
|
|
1180
|
+
setStaged(false);
|
|
1181
|
+
setReviseText("");
|
|
1182
|
+
}}>Back</button>
|
|
1183
|
+
</div>
|
|
1184
|
+
`
|
|
1185
|
+
: html`
|
|
1186
|
+
<div class="modal-actions">
|
|
1187
|
+
<button class="primary" onClick=${() => onResolve("checkpoint", "continue")}>Continue</button>
|
|
1188
|
+
<button onClick=${() => setStaged(true)}>Revise…</button>
|
|
1189
|
+
<button class="danger" onClick=${() => onResolve("checkpoint", "stop")}>Stop</button>
|
|
1190
|
+
</div>
|
|
1191
|
+
`
|
|
1192
|
+
}
|
|
1193
|
+
<//>
|
|
1194
|
+
`;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function RevisionModal({ modal, onResolve }) {
|
|
1198
|
+
const riskColor = (r) =>
|
|
1199
|
+
r === "high" ? "#f87171" : r === "med" ? "#fbbf24" : r === "low" ? "#86efac" : "#9ca3af";
|
|
1200
|
+
return html`
|
|
1201
|
+
<${ModalCard}
|
|
1202
|
+
accent="#c4b5fd"
|
|
1203
|
+
icon="✎"
|
|
1204
|
+
title="model proposed a plan revision"
|
|
1205
|
+
subtitle=${modal.summary || modal.reason}
|
|
1206
|
+
>
|
|
1207
|
+
<div class="modal-revise-reason">${modal.reason}</div>
|
|
1208
|
+
<ol class="modal-revise-steps">
|
|
1209
|
+
${modal.remainingSteps.map(
|
|
1210
|
+
(s) => html`
|
|
1211
|
+
<li key=${s.id}>
|
|
1212
|
+
<span class="modal-revise-dot" style=${`background:${riskColor(s.risk)}`}></span>
|
|
1213
|
+
<span class="modal-revise-id">${s.id}</span>
|
|
1214
|
+
<span class="modal-revise-title">${s.title}</span>
|
|
1215
|
+
<span class="modal-revise-action">${s.action}</span>
|
|
1216
|
+
</li>
|
|
1217
|
+
`,
|
|
1218
|
+
)}
|
|
1219
|
+
</ol>
|
|
1220
|
+
<div class="modal-actions">
|
|
1221
|
+
<button class="primary" onClick=${() => onResolve("revision", "accept")}>Accept</button>
|
|
1222
|
+
<button class="danger" onClick=${() => onResolve("revision", "reject")}>Reject</button>
|
|
1223
|
+
</div>
|
|
1224
|
+
<//>
|
|
1225
|
+
`;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1139
1228
|
function ChatPanel() {
|
|
1140
1229
|
const [messages, setMessages] = useState([]);
|
|
1141
1230
|
const [streaming, setStreaming] = useState(null); // { id, text, reasoning }
|
|
1231
|
+
// Tool currently dispatched but not yet returning. Set on `tool_start`,
|
|
1232
|
+
// cleared on `tool` / `error`. Drives the in-flight row so the user
|
|
1233
|
+
// sees what's running (path, command, char counts) instead of a
|
|
1234
|
+
// generic "waiting" — file writes especially feel hung otherwise.
|
|
1235
|
+
const [activeTool, setActiveTool] = useState(null); // { id, toolName, args }
|
|
1142
1236
|
const [busy, setBusy] = useState(false);
|
|
1143
1237
|
const [input, setInput] = useState("");
|
|
1144
1238
|
const [error, setError] = useState(null);
|
|
@@ -1278,17 +1372,16 @@ function ChatPanel() {
|
|
|
1278
1372
|
return;
|
|
1279
1373
|
}
|
|
1280
1374
|
if (dash.kind === "tool_start") {
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
},
|
|
1288
|
-
]);
|
|
1375
|
+
// Surface the dispatched tool + its args in the in-flight row.
|
|
1376
|
+
// No info-row placeholder: the InFlightRow now renders the
|
|
1377
|
+
// detail (path / command / char count) and the result card
|
|
1378
|
+
// appears when the `tool` event lands. Two rows for one tool
|
|
1379
|
+
// call was redundant noise.
|
|
1380
|
+
setActiveTool({ id: dash.id, toolName: dash.toolName, args: dash.args });
|
|
1289
1381
|
return;
|
|
1290
1382
|
}
|
|
1291
1383
|
if (dash.kind === "tool") {
|
|
1384
|
+
setActiveTool((cur) => (cur && cur.id === dash.id ? null : cur));
|
|
1292
1385
|
setMessages((prev) => [
|
|
1293
1386
|
...prev,
|
|
1294
1387
|
{
|
|
@@ -1302,6 +1395,9 @@ function ChatPanel() {
|
|
|
1302
1395
|
return;
|
|
1303
1396
|
}
|
|
1304
1397
|
if (dash.kind === "warning" || dash.kind === "error" || dash.kind === "info") {
|
|
1398
|
+
if (dash.kind === "error") {
|
|
1399
|
+
setActiveTool(null);
|
|
1400
|
+
}
|
|
1305
1401
|
setMessages((prev) => [...prev, { id: dash.id, role: dash.kind, text: dash.text }]);
|
|
1306
1402
|
return;
|
|
1307
1403
|
}
|
|
@@ -1368,6 +1464,7 @@ function ChatPanel() {
|
|
|
1368
1464
|
await api("/submit", { method: "POST", body: { prompt: "/new" } });
|
|
1369
1465
|
setMessages([]);
|
|
1370
1466
|
setStreaming(null);
|
|
1467
|
+
setActiveTool(null);
|
|
1371
1468
|
showToast("new conversation", "info");
|
|
1372
1469
|
// Refetch to reconcile in case the slash queued an info row.
|
|
1373
1470
|
setTimeout(async () => {
|
|
@@ -1388,6 +1485,7 @@ function ChatPanel() {
|
|
|
1388
1485
|
await api("/submit", { method: "POST", body: { prompt: "/clear" } });
|
|
1389
1486
|
setMessages([]);
|
|
1390
1487
|
setStreaming(null);
|
|
1488
|
+
setActiveTool(null);
|
|
1391
1489
|
showToast("scrollback cleared", "info");
|
|
1392
1490
|
setTimeout(async () => {
|
|
1393
1491
|
try {
|
|
@@ -1676,7 +1774,13 @@ function ChatPanel() {
|
|
|
1676
1774
|
? html`<${PlanModal} modal=${modal} onResolve=${resolveModal} />`
|
|
1677
1775
|
: modal.kind === "edit-review"
|
|
1678
1776
|
? html`<${EditReviewModal} modal=${modal} onResolve=${resolveModal} />`
|
|
1679
|
-
:
|
|
1777
|
+
: modal.kind === "workspace"
|
|
1778
|
+
? html`<${WorkspaceModal} modal=${modal} onResolve=${resolveModal} />`
|
|
1779
|
+
: modal.kind === "checkpoint"
|
|
1780
|
+
? html`<${CheckpointModal} modal=${modal} onResolve=${resolveModal} />`
|
|
1781
|
+
: modal.kind === "revision"
|
|
1782
|
+
? html`<${RevisionModal} modal=${modal} onResolve=${resolveModal} />`
|
|
1783
|
+
: null
|
|
1680
1784
|
: null
|
|
1681
1785
|
}
|
|
1682
1786
|
|
|
@@ -1724,6 +1828,7 @@ function ChatPanel() {
|
|
|
1724
1828
|
busy
|
|
1725
1829
|
? html`<${InFlightRow}
|
|
1726
1830
|
streaming=${streaming}
|
|
1831
|
+
activeTool=${activeTool}
|
|
1727
1832
|
startedAt=${turnStartedAt}
|
|
1728
1833
|
statusLine=${statusLine}
|
|
1729
1834
|
onAbort=${abort}
|
|
@@ -1736,18 +1841,55 @@ function ChatPanel() {
|
|
|
1736
1841
|
`;
|
|
1737
1842
|
}
|
|
1738
1843
|
|
|
1844
|
+
// Summarize the dispatched tool in one line — what the user wants to
|
|
1845
|
+
// know is "is this hung or really doing X". Per-tool projection so a
|
|
1846
|
+
// write_file says "→ /path/foo (12,345 ch)" instead of just "tool is
|
|
1847
|
+
// running". Returns null for tools we don't have a custom shape for;
|
|
1848
|
+
// the row falls back to the bare tool name.
|
|
1849
|
+
function summarizeActiveTool(activeTool) {
|
|
1850
|
+
if (!activeTool) return null;
|
|
1851
|
+
const name = activeTool.toolName ?? "tool";
|
|
1852
|
+
const args = parseToolArgs(activeTool.args);
|
|
1853
|
+
const path = args?.path ?? args?.file_path ?? args?.filename;
|
|
1854
|
+
if (name === "write_file" && path) {
|
|
1855
|
+
const len = typeof args?.content === "string" ? args.content.length : null;
|
|
1856
|
+
return `${name} → ${path}${len != null ? ` (${len.toLocaleString()} ch)` : ""}`;
|
|
1857
|
+
}
|
|
1858
|
+
if ((name === "edit_file" || name.endsWith("_edit_file")) && path) {
|
|
1859
|
+
return `${name} → ${path}`;
|
|
1860
|
+
}
|
|
1861
|
+
if ((name === "run_command" || name === "run_background") && typeof args?.command === "string") {
|
|
1862
|
+
const c = args.command;
|
|
1863
|
+
return `${name} → $ ${c.length > 80 ? `${c.slice(0, 80)}…` : c}`;
|
|
1864
|
+
}
|
|
1865
|
+
if ((name === "read_file" || name === "list_files" || name === "search_files") && path) {
|
|
1866
|
+
return `${name} → ${path}`;
|
|
1867
|
+
}
|
|
1868
|
+
if (path) return `${name} → ${path}`;
|
|
1869
|
+
return name;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1739
1872
|
// Live "what's the model doing right now" strip. Lives just above the
|
|
1740
1873
|
// ChatStatusBar so the user's eyes don't have to leave the input area
|
|
1741
1874
|
// to see whether the turn is alive — ticks every 500ms via the parent's
|
|
1742
1875
|
// nowTick so the seconds counter shows visible motion even when the
|
|
1743
1876
|
// SSE stream is silent (model thinking, waiting on a tool, etc).
|
|
1744
|
-
function InFlightRow({ streaming, startedAt, statusLine, onAbort, tick: _tick }) {
|
|
1877
|
+
function InFlightRow({ streaming, activeTool, startedAt, statusLine, onAbort, tick: _tick }) {
|
|
1745
1878
|
const elapsedMs = startedAt ? Date.now() - startedAt : 0;
|
|
1746
1879
|
const elapsed = (elapsedMs / 1000).toFixed(1);
|
|
1747
1880
|
const reasoningLen = streaming?.reasoning?.length ?? 0;
|
|
1748
1881
|
const textLen = streaming?.text?.length ?? 0;
|
|
1749
|
-
|
|
1750
|
-
|
|
1882
|
+
// Tool-running phase wins over text/reasoning since the model is
|
|
1883
|
+
// blocked on the tool — even if assistant_delta has fired we want
|
|
1884
|
+
// to show the active dispatch.
|
|
1885
|
+
const toolSummary = summarizeActiveTool(activeTool);
|
|
1886
|
+
const phase = toolSummary
|
|
1887
|
+
? "running"
|
|
1888
|
+
: reasoningLen > 0 && textLen === 0
|
|
1889
|
+
? "thinking"
|
|
1890
|
+
: textLen > 0
|
|
1891
|
+
? "streaming"
|
|
1892
|
+
: "waiting";
|
|
1751
1893
|
return html`
|
|
1752
1894
|
<div class="chat-inflight">
|
|
1753
1895
|
<span class="spinner"></span>
|
|
@@ -1755,7 +1897,15 @@ function InFlightRow({ streaming, startedAt, statusLine, onAbort, tick: _tick })
|
|
|
1755
1897
|
<span class="chat-inflight-sep">·</span>
|
|
1756
1898
|
<span class="muted">${elapsed}s</span>
|
|
1757
1899
|
${
|
|
1758
|
-
|
|
1900
|
+
toolSummary
|
|
1901
|
+
? html`
|
|
1902
|
+
<span class="chat-inflight-sep">·</span>
|
|
1903
|
+
<span class="chat-inflight-tool" title=${toolSummary}>${toolSummary}</span>
|
|
1904
|
+
`
|
|
1905
|
+
: null
|
|
1906
|
+
}
|
|
1907
|
+
${
|
|
1908
|
+
!toolSummary && (textLen > 0 || reasoningLen > 0)
|
|
1759
1909
|
? html`
|
|
1760
1910
|
<span class="chat-inflight-sep">·</span>
|
|
1761
1911
|
<span class="muted">
|
|
@@ -4229,23 +4379,41 @@ function ErrorOverlay() {
|
|
|
4229
4379
|
|
|
4230
4380
|
// Preact ErrorBoundary — catches render-time exceptions in the App
|
|
4231
4381
|
// subtree and dispatches them to the error overlay instead of leaving
|
|
4232
|
-
// the user with a blank white page.
|
|
4233
|
-
//
|
|
4382
|
+
// the user with a blank white page. Recovers automatically the first
|
|
4383
|
+
// few times so transient hiccups don't strand the user; if a panel
|
|
4384
|
+
// throws repeatedly we stop the loop and render a manual "Try again"
|
|
4385
|
+
// fallback so the page never looks blank-but-ticking.
|
|
4234
4386
|
class ErrorBoundary extends Component {
|
|
4235
4387
|
constructor(props) {
|
|
4236
4388
|
super(props);
|
|
4237
|
-
this.state = { caught: false };
|
|
4389
|
+
this.state = { caught: false, lastErr: null, attempts: 0 };
|
|
4238
4390
|
}
|
|
4239
|
-
static getDerivedStateFromError() {
|
|
4240
|
-
return { caught: true };
|
|
4391
|
+
static getDerivedStateFromError(error) {
|
|
4392
|
+
return { caught: true, lastErr: error };
|
|
4241
4393
|
}
|
|
4242
4394
|
componentDidCatch(error, info) {
|
|
4243
4395
|
reportAppError(error, "render", info?.componentStack ?? "");
|
|
4244
|
-
|
|
4245
|
-
|
|
4396
|
+
const attempts = (this.state.attempts ?? 0) + 1;
|
|
4397
|
+
if (attempts >= 3) {
|
|
4398
|
+
// Stop the auto-recover loop — the panel is genuinely broken,
|
|
4399
|
+
// surface a "Try again" button instead of flickering.
|
|
4400
|
+
this.setState({ attempts });
|
|
4401
|
+
return;
|
|
4402
|
+
}
|
|
4403
|
+
setTimeout(() => this.setState({ caught: false, attempts }), 100);
|
|
4246
4404
|
}
|
|
4247
4405
|
render() {
|
|
4248
4406
|
if (this.state.caught) {
|
|
4407
|
+
if ((this.state.attempts ?? 0) >= 3) {
|
|
4408
|
+
return html`
|
|
4409
|
+
<div class="boot" style="flex-direction: column; gap: 12px;">
|
|
4410
|
+
<div>this panel keeps crashing — the error overlay has the trace.</div>
|
|
4411
|
+
<button onClick=${() => this.setState({ caught: false, attempts: 0 })}>
|
|
4412
|
+
Try again
|
|
4413
|
+
</button>
|
|
4414
|
+
</div>
|
|
4415
|
+
`;
|
|
4416
|
+
}
|
|
4249
4417
|
return html`<div class="boot">recovering…</div>`;
|
|
4250
4418
|
}
|
|
4251
4419
|
return this.props.children;
|