mindweave 3.3.0 → 3.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/alternator/chassis/lsp.js +1 -1
- package/dist/alternator/chassis/lsp.js.map +1 -1
- package/dist/alternator/chassis/provision.js +2 -2
- package/dist/alternator/chassis/provision.js.map +1 -1
- package/dist/cli/App.js +221 -57
- package/dist/cli/App.js.map +1 -1
- package/dist/cli/attachments.js +2 -1
- package/dist/cli/attachments.js.map +1 -1
- package/dist/cli/blockHeights.js +14 -0
- package/dist/cli/blockHeights.js.map +1 -1
- package/dist/cli/components/BlockView.js +9 -1
- package/dist/cli/components/BlockView.js.map +1 -1
- package/dist/cli/components/EditGroup.js +50 -0
- package/dist/cli/components/EditGroup.js.map +1 -0
- package/dist/cli/components/FadeText.js +47 -0
- package/dist/cli/components/FadeText.js.map +1 -0
- package/dist/cli/components/PromptInput.js +26 -4
- package/dist/cli/components/PromptInput.js.map +1 -1
- package/dist/cli/components/ToolLine.js +5 -3
- package/dist/cli/components/ToolLine.js.map +1 -1
- package/dist/cli/components/WorkGroup.js +34 -23
- package/dist/cli/components/WorkGroup.js.map +1 -1
- package/dist/cli/diffRows.js +16 -0
- package/dist/cli/diffRows.js.map +1 -1
- package/dist/cli/editGroup.js +122 -0
- package/dist/cli/editGroup.js.map +1 -0
- package/dist/cli/entrance.js +95 -0
- package/dist/cli/entrance.js.map +1 -0
- package/dist/cli/framebuffer/parse.js +99 -16
- package/dist/cli/framebuffer/parse.js.map +1 -1
- package/dist/cli/framebuffer/writer.js +46 -28
- package/dist/cli/framebuffer/writer.js.map +1 -1
- package/dist/cli/help.js +6 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inkSpeedLoader.js +9 -5
- package/dist/cli/inkSpeedLoader.js.map +1 -1
- package/dist/cli/inkThrottlePatch.js +43 -0
- package/dist/cli/inkThrottlePatch.js.map +1 -0
- package/dist/cli/screenMode.js +8 -1
- package/dist/cli/screenMode.js.map +1 -1
- package/dist/cli/sessionCarry.js.map +1 -1
- package/dist/cli/toolDisplay.js +2 -2
- package/dist/cli/toolDisplay.js.map +1 -1
- package/dist/cli/transcript.js +67 -14
- package/dist/cli/transcript.js.map +1 -1
- package/dist/cli/workLines.js +27 -4
- package/dist/cli/workLines.js.map +1 -1
- package/dist/core/turnRunner.js +98 -7
- package/dist/core/turnRunner.js.map +1 -1
- package/dist/drivers/anthropic/client.js +16 -3
- package/dist/drivers/anthropic/client.js.map +1 -1
- package/dist/drivers/anthropic/manifest.js +10 -0
- package/dist/drivers/anthropic/manifest.js.map +1 -1
- package/dist/drivers/glm/manifest.js +9 -0
- package/dist/drivers/glm/manifest.js.map +1 -1
- package/dist/drivers/groq/manifest.js +5 -0
- package/dist/drivers/groq/manifest.js.map +1 -1
- package/dist/drivers/mistral/client.js +3 -0
- package/dist/drivers/mistral/client.js.map +1 -1
- package/dist/drivers/openai/client.js +3 -0
- package/dist/drivers/openai/client.js.map +1 -1
- package/dist/drivers/openai/manifest.js +5 -0
- package/dist/drivers/openai/manifest.js.map +1 -1
- package/dist/drivers/openaiCompat/wire.js +7 -2
- package/dist/drivers/openaiCompat/wire.js.map +1 -1
- package/dist/drivers/openrouter/client.js +6 -0
- package/dist/drivers/openrouter/client.js.map +1 -1
- package/dist/drivers/xai/client.js +9 -0
- package/dist/drivers/xai/client.js.map +1 -1
- package/dist/dynamo/cacheBreak.js +8 -5
- package/dist/dynamo/cacheBreak.js.map +1 -1
- package/dist/dynamo/contextWindow.js +64 -12
- package/dist/dynamo/contextWindow.js.map +1 -1
- package/dist/dynamo/engine.js +132 -66
- package/dist/dynamo/engine.js.map +1 -1
- package/dist/dynamo/prompt.js +8 -2
- package/dist/dynamo/prompt.js.map +1 -1
- package/dist/dynamo/toolAliases.js +73 -0
- package/dist/dynamo/toolAliases.js.map +1 -0
- package/dist/dynamo/toolArgs.js +82 -0
- package/dist/dynamo/toolArgs.js.map +1 -0
- package/dist/dynamo/verify.js +16 -2
- package/dist/dynamo/verify.js.map +1 -1
- package/dist/memory/clearedArchive.js +2 -20
- package/dist/memory/clearedArchive.js.map +1 -1
- package/dist/memory/compaction.js +3 -1
- package/dist/memory/compaction.js.map +1 -1
- package/dist/memory/localCommand.js +30 -3
- package/dist/memory/localCommand.js.map +1 -1
- package/dist/memory/presence.js +44 -0
- package/dist/memory/presence.js.map +1 -1
- package/dist/memory/session.js +4 -0
- package/dist/memory/session.js.map +1 -1
- package/dist/memory/store.js +27 -4
- package/dist/memory/store.js.map +1 -1
- package/dist/memory/types.js.map +1 -1
- package/dist/memory/userNotes.js +17 -0
- package/dist/memory/userNotes.js.map +1 -0
- package/dist/tools/backgroundShells.js +38 -7
- package/dist/tools/backgroundShells.js.map +1 -1
- package/dist/tools/deferredNative.js +24 -0
- package/dist/tools/deferredNative.js.map +1 -1
- package/dist/tools/edit.js +18 -3
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/editCore.js +107 -1
- package/dist/tools/editCore.js.map +1 -1
- package/dist/tools/mcpSearch.js +8 -2
- package/dist/tools/mcpSearch.js.map +1 -1
- package/dist/tools/readFile.js +16 -1
- package/dist/tools/readFile.js.map +1 -1
- package/dist/tools/rebuildDetail.js +94 -3
- package/dist/tools/rebuildDetail.js.map +1 -1
- package/dist/tools/registry.js +12 -12
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/runCommand.js +10 -3
- package/dist/tools/runCommand.js.map +1 -1
- package/dist/tools/screenshot.js +42 -3
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/screenshotWin.js +221 -216
- package/dist/tools/screenshotWin.js.map +1 -1
- package/dist/tools/search.js +4 -1
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/shellTools.js +17 -25
- package/dist/tools/shellTools.js.map +1 -1
- package/dist/tools/ui.js +26 -9
- package/dist/tools/ui.js.map +1 -1
- package/dist/tools/uiWin.js +4 -1
- package/dist/tools/uiWin.js.map +1 -1
- package/dist/tools/untrusted.js +24 -0
- package/dist/tools/untrusted.js.map +1 -1
- package/package.json +78 -78
package/dist/cli/App.js
CHANGED
|
@@ -28,7 +28,7 @@ import { compactNow, contextUsed, effectiveAutoCompactThreshold, respond } from
|
|
|
28
28
|
import { contextPressure, sharpContextWindow } from "../dynamo/contextWindow.js";
|
|
29
29
|
import { contextBudget, formatBudget } from "../dynamo/contextBudget.js";
|
|
30
30
|
import { contextView, resetContextOverride, setContextOverride } from "../core/contextSettings.js";
|
|
31
|
-
import { createSession, resumeSession
|
|
31
|
+
import { createSession, resumeSession } from "../memory/session.js";
|
|
32
32
|
import { saveSession, listSessions } from "../memory/store.js";
|
|
33
33
|
import { stopChassis } from "../alternator/lane.js";
|
|
34
34
|
import { loadSkillBody, substituteSkillArgs } from "../governor/skills.js";
|
|
@@ -73,15 +73,16 @@ import { approvalAnswer, approvalBlock, workedSeconds } from "./approvalText.js"
|
|
|
73
73
|
import { BlockView } from "./components/BlockView.js";
|
|
74
74
|
import { initialState, lastExpandable, reduce, trimNarration } from "./transcript.js";
|
|
75
75
|
import { isTight } from "./blockSpacing.js";
|
|
76
|
-
import { parseScreenArg, screenChoices, screenNotice, startupMode } from "./screenMode.js";
|
|
76
|
+
import { INLINE_AVAILABLE, INLINE_UNAVAILABLE, parseScreenArg, screenChoices, screenNotice, startupMode } from "./screenMode.js";
|
|
77
77
|
import { applyScreenMode } from "./screenShell.js";
|
|
78
78
|
import { saveScreenMode } from "./screenStore.js";
|
|
79
79
|
import { FULL_TAIL_BLOCKS, needsMeasure, pruneHeights, seedHeights } from "./blockHeights.js";
|
|
80
|
-
import { expandableAt, hitBlock, hitItem, revealNode } from "./expandHits.js";
|
|
80
|
+
import { expandableAt, hitBlock, hitItem, itemHit, revealNode } from "./expandHits.js";
|
|
81
|
+
import { isGrouped, nameAt } from "./editGroup.js";
|
|
81
82
|
import { shellNote } from "./shellNotes.js";
|
|
82
83
|
import { pressBackground } from "./commandBackground.js";
|
|
83
84
|
import { readFileSync, statSync } from "node:fs";
|
|
84
|
-
import { compactSearchDetail, compactUiDetail, rebuildFull, writtenFromDisk } from "../tools/rebuildDetail.js";
|
|
85
|
+
import { compactSearchDetail, compactUiDetail, rebuildFull, recoverReadText, recoverSavedText, searchDetailFromContent, writtenFromDisk } from "../tools/rebuildDetail.js";
|
|
85
86
|
import { isUserShell, runUserCommand } from "../tools/userCommand.js";
|
|
86
87
|
import { isUserCommandText, parseUserCommand } from "../memory/localCommand.js";
|
|
87
88
|
import { shownText } from "../memory/compaction.js";
|
|
@@ -109,7 +110,7 @@ import { countNewReplies, hitsPill, pillBounds, scrollPill } from "./scrollPill.
|
|
|
109
110
|
import { SCROLLBACK_BLOCKS, virtualWindow } from "./virtualWindow.js";
|
|
110
111
|
import { perf, perfEnabled } from "./perfLog.js";
|
|
111
112
|
import { newPacer, nextHoldAt, nextMove, takeImmediate, takePaced } from "./revealQueue.js";
|
|
112
|
-
import { drain as drainQueue, popAll as popAllQueued, queueMessage, takeSteerable
|
|
113
|
+
import { drain as drainQueue, popAll as popAllQueued, queueMessage, takeSteerable } from "./messageQueue.js";
|
|
113
114
|
import { routeCommand, parseCommandLine, unknownCommandMessage } from "./commandRoute.js";
|
|
114
115
|
import { resolveChoice, splitModelArg } from "./commandArgs.js";
|
|
115
116
|
import { carryAcrossFreshSession } from "./sessionCarry.js";
|
|
@@ -174,11 +175,10 @@ function orderedModelsOf(providerId) {
|
|
|
174
175
|
const provider = allProviders().find((p) => p.id === providerId);
|
|
175
176
|
return provider ? orderModels(modelsOf(provider)) : [];
|
|
176
177
|
}
|
|
177
|
-
// After you pick a session in /continue, the
|
|
178
|
+
// After you pick a session in /continue, the two ways to resume it.
|
|
178
179
|
const RESUME_MODES = [
|
|
179
180
|
{ label: "Compact & continue", description: "summarize the old chat first so it won't eat your context, then pick up where you left off" },
|
|
180
181
|
{ label: "Continue as-is", description: "resume the full conversation unchanged" },
|
|
181
|
-
{ label: "Fresh start", description: "leave it and start a new empty session here instead" },
|
|
182
182
|
];
|
|
183
183
|
export function App({ resumeSessionId, initialScreen }) {
|
|
184
184
|
// The transcript state machine lives in a ref and is advanced by the reducer as
|
|
@@ -359,6 +359,14 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
359
359
|
/** The chip's cells in SCREEN coordinates, for the pointer handler. Null when there is
|
|
360
360
|
* nothing to click. Published by the render, read between frames. */
|
|
361
361
|
const pillHit = useRef(null);
|
|
362
|
+
/** The queued line's row within the live region (measured as it is drawn), and its screen row for a click. */
|
|
363
|
+
const queueRow = useRef(null);
|
|
364
|
+
// Steered messages that left the queue but have not reached the chat yet (see steerRunningTurn).
|
|
365
|
+
const [departing, setDeparting] = useState([]);
|
|
366
|
+
const departingRef = useRef(0);
|
|
367
|
+
const queueHitRow = useRef(null);
|
|
368
|
+
/** Pulls the queue back into the input box to edit (installed by the input, as the caret click is). */
|
|
369
|
+
const pullQueue = useRef(null);
|
|
362
370
|
// Where the layout's first row sits on the screen, published each render for the pointer.
|
|
363
371
|
const frameTopRef = useRef(0);
|
|
364
372
|
// The row the pointer is over, if it can be pressed. State rather than a ref: it is what
|
|
@@ -798,6 +806,12 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
798
806
|
if (compact)
|
|
799
807
|
return { detail: compact.detail, ...(compact.detailFull ? { detailFull: compact.detailFull } : {}) };
|
|
800
808
|
}
|
|
809
|
+
// A search saved before it had a row has no block of its own: what it printed is what the model was given.
|
|
810
|
+
if (call?.name === "search" && !e.detail) {
|
|
811
|
+
const printed = searchDetailFromContent(e.content) ?? recoverSavedText(e.content);
|
|
812
|
+
if (printed)
|
|
813
|
+
return { detail: printed, detailKind: "text" };
|
|
814
|
+
}
|
|
801
815
|
let full = e.detailFull ?? rebuildFull(call?.name, call?.args, e);
|
|
802
816
|
// A write whose text was cleared to save context has only the file left to open.
|
|
803
817
|
if (!full && call?.name === "write_file" && typeof call.args?.path === "string") {
|
|
@@ -813,7 +827,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
813
827
|
return full ? { detailFull: full } : {};
|
|
814
828
|
})(),
|
|
815
829
|
...(e.detailKind ? { detailKind: e.detailKind } : {}),
|
|
816
|
-
...(e.quiet ? { quiet: true } : {}),
|
|
830
|
+
...(e.quiet && !(!e.isError && !e.content.startsWith("Error:") && callsById.get(e.toolCallId)?.name === "search") ? { quiet: true } : {}),
|
|
817
831
|
...(e.displayName ? { name: e.displayName } : {}),
|
|
818
832
|
...(e.displayKind ? { action: e.displayKind } : {}),
|
|
819
833
|
...(e.awaitsModel ? { awaitsModel: true } : {}),
|
|
@@ -1823,6 +1837,12 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1823
1837
|
// On PRESS rather than release: the chip is a single row and the view moves out
|
|
1824
1838
|
// from under the pointer the instant it is hit, so a release-matched-to-press
|
|
1825
1839
|
// would be testing the pointer against a screen that had already changed.
|
|
1840
|
+
// The queued line is a button too: a click on it takes the queue back into the box to edit.
|
|
1841
|
+
if (queueHitRow.current !== null && event.y === queueHitRow.current && pullQueue.current) {
|
|
1842
|
+
clearSelection();
|
|
1843
|
+
pullQueue.current();
|
|
1844
|
+
continue;
|
|
1845
|
+
}
|
|
1826
1846
|
const hit = pillHit.current;
|
|
1827
1847
|
if (hit && hitsPill(hit, event.x, event.y)) {
|
|
1828
1848
|
clearSelection();
|
|
@@ -1917,7 +1937,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1917
1937
|
const hitId = expandableAt(event.y, frameTopRef.current, chatRef.current);
|
|
1918
1938
|
if (hitId !== null) {
|
|
1919
1939
|
lastClick.current = null;
|
|
1920
|
-
toggleRow(hitId);
|
|
1940
|
+
toggleRow(clickTarget(hitId, event.x));
|
|
1921
1941
|
continue;
|
|
1922
1942
|
}
|
|
1923
1943
|
// Plain text: two clicks in a row take the word, three take the line.
|
|
@@ -1946,7 +1966,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1946
1966
|
// the input anyway, and a click that does nothing is the worse outcome.
|
|
1947
1967
|
const hitId = expandableAt(event.y, frameTopRef.current, chatRef.current);
|
|
1948
1968
|
if (hitId !== null) {
|
|
1949
|
-
toggleRow(hitId);
|
|
1969
|
+
toggleRow(clickTarget(hitId, event.x));
|
|
1950
1970
|
continue;
|
|
1951
1971
|
}
|
|
1952
1972
|
placeCaretAt(event.x, event.y);
|
|
@@ -1970,7 +1990,9 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1970
1990
|
// the light out when it leaves. A press or a wheel turn clears it too, below, because
|
|
1971
1991
|
// the content moves out from under a pointer that did not.
|
|
1972
1992
|
if (lastMove) {
|
|
1973
|
-
|
|
1993
|
+
// On the closed header of a row of edits the pointer can be over one file's name: that name is what is pointed at.
|
|
1994
|
+
const row = expandableAt(lastMove.y, frameTopRef.current, chatRef.current);
|
|
1995
|
+
const over = row === null ? null : clickTarget(row, lastMove.x);
|
|
1974
1996
|
setHoverId((h) => (h === over ? h : over));
|
|
1975
1997
|
}
|
|
1976
1998
|
else if (notches.length > 0 || events.some((e) => e.kind === "press")) {
|
|
@@ -1981,6 +2003,56 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1981
2003
|
if (events.length === 0 && notches.length === 0)
|
|
1982
2004
|
clearSelection();
|
|
1983
2005
|
}, { isActive: true });
|
|
2006
|
+
/**
|
|
2007
|
+
* A read keeps no text of its own in the row: what it took is in the conversation, under the call's id.
|
|
2008
|
+
* It is fetched when the line is opened, so nothing is stored twice and an old chat opens as well as a new one.
|
|
2009
|
+
*/
|
|
2010
|
+
function fetchReadText(blockId, index) {
|
|
2011
|
+
const st = stateRef.current;
|
|
2012
|
+
const block = [...st.committed, ...st.tail].find((b) => b.id === blockId);
|
|
2013
|
+
const item = block && block.kind === "work" && block.items.length > 0 ? block.items[block.items.length === 1 ? 0 : index] : undefined;
|
|
2014
|
+
if (item && !item.open)
|
|
2015
|
+
fetchRead(item);
|
|
2016
|
+
}
|
|
2017
|
+
function fetchRead(item) {
|
|
2018
|
+
if (item.kind !== "read" || item.detail)
|
|
2019
|
+
return;
|
|
2020
|
+
const entry = session.current?.transcript.find((t) => t.role === "tool" && t.toolCallId === item.toolId);
|
|
2021
|
+
// Not recorded yet (the call is still finishing): nothing to fetch, and nothing to claim.
|
|
2022
|
+
if (!entry)
|
|
2023
|
+
return;
|
|
2024
|
+
// Clearing context empties what the MODEL carries; the screen keeps what was read: the copy clearing saved, or the file.
|
|
2025
|
+
const call = [...(session.current?.transcript ?? [])].reverse().flatMap((t) => (t.role === "assistant" ? t.toolCalls ?? [] : [])).find((c) => c.id === item.toolId);
|
|
2026
|
+
let args;
|
|
2027
|
+
try {
|
|
2028
|
+
args = call ? JSON.parse(call.arguments) : undefined;
|
|
2029
|
+
}
|
|
2030
|
+
catch {
|
|
2031
|
+
args = undefined;
|
|
2032
|
+
}
|
|
2033
|
+
const text = searchDetailFromContent(entry.content) ??
|
|
2034
|
+
recoverReadText(entry.content, { args }, session.current?.cwd ?? process.cwd(), (p) => { try {
|
|
2035
|
+
return statSync(p).size < 4_000_000 ? readFileSync(p, "utf8") : undefined;
|
|
2036
|
+
}
|
|
2037
|
+
catch {
|
|
2038
|
+
return undefined;
|
|
2039
|
+
} }, (cwd, p) => resolve(cwd, p));
|
|
2040
|
+
dispatch({ type: "itemDetail", toolId: item.toolId, detail: text ?? "(this file is not on disk any more, and the copy saved when the chat was cleared is gone too)" });
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* A click on the closed header of a row of several edits can land on one file's name: then it is that file's change that opens.
|
|
2044
|
+
* The header is drawn from the same layout that answers here (editGroup.ts), so the two cannot disagree.
|
|
2045
|
+
*/
|
|
2046
|
+
function clickTarget(hit, x) {
|
|
2047
|
+
if (hit < 0)
|
|
2048
|
+
return hit;
|
|
2049
|
+
const st = stateRef.current;
|
|
2050
|
+
const block = [...st.committed, ...st.tail].find((b) => b.id === hit);
|
|
2051
|
+
if (!block || block.kind !== "edits" || block.open || !isGrouped(block.items))
|
|
2052
|
+
return hit;
|
|
2053
|
+
const at = nameAt(block.items, width, x, block.live);
|
|
2054
|
+
return at >= 0 ? itemHit(hit, at) : hit;
|
|
2055
|
+
}
|
|
1984
2056
|
/** Open or fold a row, keeping it where the reader pressed it. See the height effect. */
|
|
1985
2057
|
function toggleRow(hit) {
|
|
1986
2058
|
rowToggled.current = true;
|
|
@@ -1990,8 +2062,18 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
1990
2062
|
// for the old height. Keep that one off the screen; the effect below releases it.
|
|
1991
2063
|
holdFrames();
|
|
1992
2064
|
// A hit on one command inside a commands row opens that command; anything else is a row.
|
|
1993
|
-
if (hitItem(hit)
|
|
2065
|
+
if (hitItem(hit) < 0) {
|
|
2066
|
+
// A row of one read shows its text as soon as it opens, so the text is fetched with the click that opens it.
|
|
2067
|
+
const st = stateRef.current;
|
|
2068
|
+
const block = [...st.committed, ...st.tail].find((b) => b.id === hit);
|
|
2069
|
+
if (block && block.kind === "work" && !block.open && block.items.length === 1)
|
|
2070
|
+
fetchRead(block.items[0]);
|
|
2071
|
+
}
|
|
2072
|
+
if (hitItem(hit) >= 0) {
|
|
2073
|
+
// Before the toggle: the state is updated the moment it is dispatched, and a read already open is not fetched for.
|
|
2074
|
+
fetchReadText(hitBlock(hit), hitItem(hit));
|
|
1994
2075
|
dispatch({ type: "toggleItem", id: hitBlock(hit), index: hitItem(hit) });
|
|
2076
|
+
}
|
|
1995
2077
|
else
|
|
1996
2078
|
dispatch({ type: "toggleExpand", id: hit });
|
|
1997
2079
|
}
|
|
@@ -2228,6 +2310,8 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2228
2310
|
holdTimer.current = null;
|
|
2229
2311
|
revealQ.current = [];
|
|
2230
2312
|
pacer.current = newPacer(revealQ.current);
|
|
2313
|
+
departingRef.current = 0;
|
|
2314
|
+
setDeparting([]);
|
|
2231
2315
|
}
|
|
2232
2316
|
function enqueueReveal(a) {
|
|
2233
2317
|
revealQ.current.push(a);
|
|
@@ -2277,6 +2361,13 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2277
2361
|
// same de-sync risk a reveal carries, so the banner is healed on the same frame.
|
|
2278
2362
|
requestFullRepaint();
|
|
2279
2363
|
applyBatch(actions);
|
|
2364
|
+
// A steered message that has just reached the chat stops showing in the queue line.
|
|
2365
|
+
const arrived = departingRef.current > 0 ? actions.filter((a) => a.type === "user").length : 0;
|
|
2366
|
+
if (arrived > 0) {
|
|
2367
|
+
const n = Math.min(arrived, departingRef.current);
|
|
2368
|
+
departingRef.current -= n;
|
|
2369
|
+
setDeparting((d) => d.slice(n));
|
|
2370
|
+
}
|
|
2280
2371
|
}
|
|
2281
2372
|
function pace() {
|
|
2282
2373
|
return {
|
|
@@ -2364,32 +2455,41 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2364
2455
|
* rows around it.
|
|
2365
2456
|
*/
|
|
2366
2457
|
async function steerRunningTurn(s) {
|
|
2458
|
+
// Never into a turn that has been stopped (Esc, ctrl+F, an interrupted command). Handed over at its
|
|
2459
|
+
// last step boundary, a message queued BEFORE the stop was drawn in the chat, put in the transcript
|
|
2460
|
+
// and never answered, because nothing runs after a stopped turn. It stays queued and goes out as
|
|
2461
|
+
// the next turn, which is what the person who typed it expects.
|
|
2462
|
+
if (abortRef.current?.signal.aborted)
|
|
2463
|
+
return [];
|
|
2367
2464
|
const { send, rest } = takeSteerable(queueRef.current);
|
|
2368
2465
|
if (send.length === 0)
|
|
2369
2466
|
return [];
|
|
2370
2467
|
queueRef.current = rest;
|
|
2371
|
-
setQueued(rest);
|
|
2372
2468
|
const out = [];
|
|
2469
|
+
const shown = [];
|
|
2373
2470
|
for (const { text } of send) {
|
|
2374
2471
|
// No history write here: `onSend` records every message the moment it is typed,
|
|
2375
2472
|
// queued or not, so ↑ walks them in the order they were written rather than the
|
|
2376
2473
|
// order they happened to go out. Recording again on the way out appended each one
|
|
2377
2474
|
// a second time.
|
|
2378
2475
|
const { content, displayText, notes, images } = await prepareMessage(s, text);
|
|
2379
|
-
|
|
2380
|
-
for (const n of notes)
|
|
2381
|
-
enqueueReveal({ type: "note", text: n });
|
|
2476
|
+
shown.push({ type: "user", text: displayText }, ...notes.map((n) => ({ type: "note", text: n })));
|
|
2382
2477
|
out.push({ content, ...(images.length > 0 ? { images } : {}) });
|
|
2383
2478
|
}
|
|
2479
|
+
// The message leaves the queue line in the same paint it reaches the chat, so it is never on
|
|
2480
|
+
// neither. Behind other rows it waits its beat in the chat's order, so the queue line keeps
|
|
2481
|
+
// showing it (`departing`) until then.
|
|
2482
|
+
setDeparting((d) => [...d, ...send]);
|
|
2483
|
+
departingRef.current += send.length;
|
|
2484
|
+
setQueued([...queueRef.current]);
|
|
2485
|
+
for (const a of shown)
|
|
2486
|
+
enqueueReveal(a);
|
|
2384
2487
|
return out;
|
|
2385
2488
|
}
|
|
2386
2489
|
async function streamRespond(s, run) {
|
|
2387
2490
|
startTurn();
|
|
2388
|
-
//
|
|
2389
|
-
//
|
|
2390
|
-
// not: it rewrites the system prompt string, which discards the entire cached prefix
|
|
2391
|
-
// (base prompt, tool schemas, project snapshot) at 1.25x rewrite cost.
|
|
2392
|
-
await reloadProjectMemory(s).catch(() => { });
|
|
2491
|
+
// No MINDWEAVE.md re-read here: an edit the model made waits for the next compaction, because
|
|
2492
|
+
// re-reading rewrites the system prompt and discards the whole cached prefix (see runTurn).
|
|
2393
2493
|
resetPacer();
|
|
2394
2494
|
lastRevealAt.current = 0;
|
|
2395
2495
|
streamDone.current = false;
|
|
@@ -2455,7 +2555,8 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2455
2555
|
detail: e.detail,
|
|
2456
2556
|
detailKind: e.detailKind,
|
|
2457
2557
|
detailFull: e.detailFull,
|
|
2458
|
-
|
|
2558
|
+
// A search that worked is a line in the work row; one that failed stays out of sight.
|
|
2559
|
+
quiet: e.quiet && !(e.name === "search" && !e.error),
|
|
2459
2560
|
action: e.displayKind,
|
|
2460
2561
|
name: e.displayName,
|
|
2461
2562
|
});
|
|
@@ -2654,6 +2755,10 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2654
2755
|
await stopCurrentLanes();
|
|
2655
2756
|
attachApproval(resumed);
|
|
2656
2757
|
session.current = resumed;
|
|
2758
|
+
// The model now sees only the picked session. Leaving the conversation it replaced above the
|
|
2759
|
+
// replayed one made each further /continue stack another on top, with the old one still
|
|
2760
|
+
// reachable by scrolling up and reading as part of this one.
|
|
2761
|
+
wipeChat();
|
|
2657
2762
|
if (compactFirst) {
|
|
2658
2763
|
startTurn();
|
|
2659
2764
|
note("compacting the old conversation so it fits…");
|
|
@@ -2677,11 +2782,8 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2677
2782
|
/**
|
|
2678
2783
|
* Drop the current chat and start a clean session in the same project.
|
|
2679
2784
|
*
|
|
2680
|
-
*
|
|
2681
|
-
*
|
|
2682
|
-
* sessions, so the history above is context for the choice you just made. Reached
|
|
2683
|
-
* through `/clear`, you asked for it to be gone, and leaving it on screen while the
|
|
2684
|
-
* model can no longer see it is the worst of both: it reads as still being there.
|
|
2785
|
+
* The screen is wiped with it: leaving the old conversation visible while the model
|
|
2786
|
+
* can no longer see it is the worst of both, it reads as still being there.
|
|
2685
2787
|
*
|
|
2686
2788
|
* What SURVIVES is as deliberate as what goes, and none of it is automatic — a fresh
|
|
2687
2789
|
* session starts genuinely empty, so anything that outlives the conversation has to
|
|
@@ -2698,7 +2800,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2698
2800
|
* So they are killed, and `/clear` SAYS how many, because silently stopping someone's
|
|
2699
2801
|
* dev server is exactly the kind of surprise a one-word command should not spring.
|
|
2700
2802
|
*/
|
|
2701
|
-
async function startFresh(
|
|
2803
|
+
async function startFresh() {
|
|
2702
2804
|
const s = session.current;
|
|
2703
2805
|
if (!s)
|
|
2704
2806
|
return;
|
|
@@ -2719,8 +2821,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2719
2821
|
// old session no longer claims turns the new one can undo.
|
|
2720
2822
|
fresh.toolContext.checkpoints?.persistTo(checkpointDir(fresh.cwd, fresh.id));
|
|
2721
2823
|
session.current = fresh;
|
|
2722
|
-
|
|
2723
|
-
wipeChat();
|
|
2824
|
+
wipeChat();
|
|
2724
2825
|
note(killed > 0
|
|
2725
2826
|
? `— started a fresh session (stopped ${killed} background command${killed === 1 ? "" : "s"}) —`
|
|
2726
2827
|
: "— started a fresh session —");
|
|
@@ -2837,10 +2938,8 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2837
2938
|
text = `${text}${text ? " " : ""}${dropHandles.current.register(attached)}`;
|
|
2838
2939
|
return text;
|
|
2839
2940
|
}
|
|
2840
|
-
// Apply the resume-mode pick for a chosen session: compact & continue / as-is
|
|
2941
|
+
// Apply the resume-mode pick for a chosen session: compact & continue / as-is.
|
|
2841
2942
|
async function applyResume(meta, mode) {
|
|
2842
|
-
if (mode === 2)
|
|
2843
|
-
return startFresh();
|
|
2844
2943
|
return resumePicked(meta, mode === 0);
|
|
2845
2944
|
}
|
|
2846
2945
|
// Apply a /model pick: switch model (clamping reasoning to a valid level), persist
|
|
@@ -2944,6 +3043,10 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
2944
3043
|
* watches `shell`. Two copies is how one of them ends up switching without remembering.
|
|
2945
3044
|
*/
|
|
2946
3045
|
function applyScreen(next) {
|
|
3046
|
+
if (next === "inline" && !INLINE_AVAILABLE) {
|
|
3047
|
+
note(`inline is ${INLINE_UNAVAILABLE}`);
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
2947
3050
|
if (next === shell) {
|
|
2948
3051
|
note(`already ${screenNotice(next)}`);
|
|
2949
3052
|
return;
|
|
@@ -3181,11 +3284,9 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
3181
3284
|
note(ANALYTICS_EXPLANATION);
|
|
3182
3285
|
return;
|
|
3183
3286
|
}
|
|
3184
|
-
// /clear — a fresh conversation without leaving the folder
|
|
3185
|
-
// way to start over was to quit and relaunch, or to go through /continue and pick
|
|
3186
|
-
// its third option, which is not somewhere anyone looks for "start over".
|
|
3287
|
+
// /clear — a fresh conversation without leaving the folder, and the one place to start over.
|
|
3187
3288
|
if (name === "/clear") {
|
|
3188
|
-
await startFresh(
|
|
3289
|
+
await startFresh();
|
|
3189
3290
|
return;
|
|
3190
3291
|
}
|
|
3191
3292
|
// /init — have the model write MINDWEAVE.md. The file is read into the cached
|
|
@@ -3286,9 +3387,9 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
3286
3387
|
const overhead = measured && measured.model === s.modelConfig.model ? measured.tokens : undefined;
|
|
3287
3388
|
const budget = contextBudget(s.transcript, overhead);
|
|
3288
3389
|
note("context breakdown:");
|
|
3289
|
-
say(`${formatBudget(budget, sharpContextWindow(s.modelConfig.model), effectiveAutoCompactThreshold(s))}
|
|
3290
|
-
|
|
3291
|
-
/context project shows what Mindweave read about this directory at startup.
|
|
3390
|
+
say(`${formatBudget(budget, sharpContextWindow(s.modelConfig.model), effectiveAutoCompactThreshold(s))}
|
|
3391
|
+
|
|
3392
|
+
/context project shows what Mindweave read about this directory at startup.
|
|
3292
3393
|
/context limit shows or sets your own compaction limit.`);
|
|
3293
3394
|
return;
|
|
3294
3395
|
}
|
|
@@ -3890,6 +3991,36 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
3890
3991
|
}
|
|
3891
3992
|
void handleSubmit(text);
|
|
3892
3993
|
}
|
|
3994
|
+
/**
|
|
3995
|
+
* ctrl+F: send NOW, the way the queue sends, at the first moment that loses nothing.
|
|
3996
|
+
*
|
|
3997
|
+
* It is the queue, not a second mechanism: the message goes in the same queue and reaches the agent
|
|
3998
|
+
* by the same road (handed to the running turn when its step ends). What it adds is a way to make
|
|
3999
|
+
* that moment come SOONER, and the only thing that holds a step up for long without anyone's
|
|
4000
|
+
* leave is a command the turn is waiting on. That is moved to the background, as ctrl+X moves it:
|
|
4001
|
+
* it keeps running, the step ends at once, the message arrives, and the agent is told the command
|
|
4002
|
+
* is still going and hears when it ends. Nothing is stopped, killed or thrown away; only Esc, or
|
|
4003
|
+
* the person asking, does that.
|
|
4004
|
+
*
|
|
4005
|
+
* A reply being written, a compaction or a sub-agent has no such handle and is left alone: the
|
|
4006
|
+
* message arrives the moment it ends, exactly as it would have through Enter.
|
|
4007
|
+
*/
|
|
4008
|
+
function forceSend(text) {
|
|
4009
|
+
if (!busy && queueRef.current.length === 0) {
|
|
4010
|
+
// Idle: nothing is in the way, so it is Enter.
|
|
4011
|
+
if (text)
|
|
4012
|
+
onSend(text);
|
|
4013
|
+
return;
|
|
4014
|
+
}
|
|
4015
|
+
if (text) {
|
|
4016
|
+
setScrollUp(0);
|
|
4017
|
+
setHistory((h) => (h[h.length - 1] === text ? h : [...h, text]));
|
|
4018
|
+
queueRef.current.push(queueMessage(text, { interrupting: interrupting.current }));
|
|
4019
|
+
setQueued([...queueRef.current]);
|
|
4020
|
+
}
|
|
4021
|
+
if (busy)
|
|
4022
|
+
pressBackground(session.current?.toolContext.foregroundCommands);
|
|
4023
|
+
}
|
|
3893
4024
|
// Autocomplete entries: the built-in slash commands, this project's skills, and any
|
|
3894
4025
|
// prompts the connected MCP servers offer (all read from the live session, so a freshly
|
|
3895
4026
|
// created skill — or a server that just finished connecting — shows up next render).
|
|
@@ -3942,36 +4073,36 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
3942
4073
|
return null;
|
|
3943
4074
|
const cur = session.current;
|
|
3944
4075
|
if (overlay.kind === "app") {
|
|
3945
|
-
return (_jsx(Picker, { title: "Mindweave desktop app", note: APP_EXPLANATION, items: [{ label: "Open the download page", description: "opens your default browser" }], width: width, maxRows: maxRows, initialIndex: 0, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4076
|
+
return (_jsx(Picker, { title: "Mindweave desktop app", note: APP_EXPLANATION, items: [{ label: "Open the download page", description: "opens your default browser" }], width: width, maxRows: maxRows, initialIndex: 0, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3946
4077
|
}
|
|
3947
4078
|
if (overlay.kind === "rewind") {
|
|
3948
4079
|
const items = overlay.items.map((p) => ({
|
|
3949
4080
|
label: p.text,
|
|
3950
4081
|
description: [timeAgo(p.at), changeSummary(p)].filter(Boolean).join(" · "),
|
|
3951
4082
|
}));
|
|
3952
|
-
return (_jsx(Picker, { title: "Rewind to before which message?", items: items, width: width, maxRows: maxRows, rightAlignDescription: true, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4083
|
+
return (_jsx(Picker, { title: "Rewind to before which message?", items: items, width: width, maxRows: maxRows, rightAlignDescription: true, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3953
4084
|
}
|
|
3954
4085
|
if (overlay.kind === "rewindConfirm") {
|
|
3955
4086
|
const p = overlay.point;
|
|
3956
4087
|
const quoted = p.text.length > 48 ? `${p.text.slice(0, 47)}…` : p.text;
|
|
3957
|
-
return (_jsx(Picker, { title: `Rewind to before “${quoted}”?`, items: rewindChoices(p).map(({ label, description }) => ({ label, description })), describeSelection: true, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4088
|
+
return (_jsx(Picker, { title: `Rewind to before “${quoted}”?`, items: rewindChoices(p).map(({ label, description }) => ({ label, description })), describeSelection: true, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3958
4089
|
}
|
|
3959
4090
|
if (overlay.kind === "sessions") {
|
|
3960
4091
|
const items = overlay.items.map((m) => ({
|
|
3961
4092
|
label: sessionTitle(m),
|
|
3962
4093
|
description: `${timeAgo(m.updatedAt)} · ${m.entryCount} msg${m.entryCount === 1 ? "" : "s"}`,
|
|
3963
4094
|
}));
|
|
3964
|
-
return (_jsx(Picker, { title: "Continue which session?", items: items, width: width, maxRows: maxRows, rightAlignDescription: true, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4095
|
+
return (_jsx(Picker, { title: "Continue which session?", items: items, width: width, maxRows: maxRows, rightAlignDescription: true, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3965
4096
|
}
|
|
3966
4097
|
if (overlay.kind === "resumeMode") {
|
|
3967
|
-
return (_jsx(Picker, { title: `Continue “${sessionTitle(overlay.meta)}” — how?`, items: RESUME_MODES, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4098
|
+
return (_jsx(Picker, { title: `Continue “${sessionTitle(overlay.meta)}” — how?`, items: RESUME_MODES, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3968
4099
|
}
|
|
3969
4100
|
if (overlay.kind === "shells") {
|
|
3970
4101
|
const items = overlay.items.map((sh) => ({
|
|
3971
4102
|
label: `#${sh.id} ${clipCmd(sh.command)}`,
|
|
3972
4103
|
description: sh.status === "running" ? `running ${shellElapsed(sh)} — Enter to stop` : sh.status === "killed" ? "killed" : `exited ${sh.exitCode}`,
|
|
3973
4104
|
}));
|
|
3974
|
-
return (_jsx(Picker, { title: "Background shells", items: items, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4105
|
+
return (_jsx(Picker, { title: "Background shells", items: items, width: width, maxRows: maxRows, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3975
4106
|
}
|
|
3976
4107
|
if (overlay.kind === "provider") {
|
|
3977
4108
|
const activeModel = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;
|
|
@@ -3992,7 +4123,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
3992
4123
|
const here = p.id === active ? ` · on ${activeLabel}` : "";
|
|
3993
4124
|
return { label: p.label + (p.id === active ? " ✓" : ""), description: `${models} · ${key}${here}` };
|
|
3994
4125
|
});
|
|
3995
|
-
return (_jsx(Picker, { title: "Choose a provider", items: items, width: width, maxRows: maxRows, searchBox: providerSearch, initialIndex: Math.max(0, providers.findIndex((p) => p.id === active)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4126
|
+
return (_jsx(Picker, { title: "Choose a provider", items: items, width: width, maxRows: maxRows, searchBox: providerSearch, initialIndex: Math.max(0, providers.findIndex((p) => p.id === active)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
3996
4127
|
}
|
|
3997
4128
|
if (overlay.kind === "model") {
|
|
3998
4129
|
const id = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;
|
|
@@ -4016,14 +4147,14 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
4016
4147
|
keywords: m.id,
|
|
4017
4148
|
};
|
|
4018
4149
|
});
|
|
4019
|
-
return (_jsx(Picker, { title: `Choose ${/^[aeiou]/i.test(pickerProvider) ? "an" : "a"} ${pickerProvider} model`, items: items, width: width, maxRows: maxRows, initialIndex: Math.max(0, models.findIndex((m) => m.id === id)), initialFilter: overlay.filter, searchBox: modelSearch, onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4150
|
+
return (_jsx(Picker, { title: `Choose ${/^[aeiou]/i.test(pickerProvider) ? "an" : "a"} ${pickerProvider} model`, items: items, width: width, maxRows: maxRows, initialIndex: Math.max(0, models.findIndex((m) => m.id === id)), initialFilter: overlay.filter, searchBox: modelSearch, onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
4020
4151
|
}
|
|
4021
4152
|
if (overlay.kind === "think") {
|
|
4022
4153
|
const model = cur?.modelConfig.model ?? DEFAULT_MODEL_CONFIG.model;
|
|
4023
4154
|
const levels = thinkLevels(model);
|
|
4024
4155
|
const curLabel = cur ? thinkLabel(cur.modelConfig) : "";
|
|
4025
4156
|
const items = levels.map((l) => ({ label: l.label + (l.label === curLabel ? " ✓" : ""), description: l.description }));
|
|
4026
|
-
return (_jsx(Picker, { title: `Reasoning for ${modelLabel(model)}`, items: items, width: width, maxRows: maxRows, initialIndex: Math.max(0, levels.findIndex((l) => l.label === curLabel)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4157
|
+
return (_jsx(Picker, { title: `Reasoning for ${modelLabel(model)}`, items: items, width: width, maxRows: maxRows, initialIndex: Math.max(0, levels.findIndex((l) => l.label === curLabel)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
4027
4158
|
}
|
|
4028
4159
|
if (overlay.kind === "screen") {
|
|
4029
4160
|
const choices = screenChoices(shell);
|
|
@@ -4033,7 +4164,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
4033
4164
|
describeSelection: true,
|
|
4034
4165
|
// Opens on the one in use, so Enter alone changes nothing. A chooser that opens
|
|
4035
4166
|
// somewhere else turns a glance at the options into an accidental switch.
|
|
4036
|
-
initialIndex: Math.max(0, choices.findIndex((c) => c.mode === shell)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }));
|
|
4167
|
+
initialIndex: Math.max(0, choices.findIndex((c) => c.mode === shell)), onSelect: onOverlaySelect, onCancel: onOverlayCancel }, overlay.kind));
|
|
4037
4168
|
}
|
|
4038
4169
|
// approval — a plan, a Sentinel action, a forbidden-path lift. It interrupts the user's
|
|
4039
4170
|
// work and the answer commits them to something, so it reads as a stop; it renders in
|
|
@@ -4180,6 +4311,7 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
4180
4311
|
const frameTop = readingInline ? Math.max(0, rows - frameHeight) : 0;
|
|
4181
4312
|
frameTopRef.current = frameTop;
|
|
4182
4313
|
pillHit.current = pill !== null && pillRow.current !== null ? pillBounds(pill, width, frameTop + pillRow.current) : null;
|
|
4314
|
+
queueHitRow.current = queueRow.current !== null ? frameTop + queueRow.current : null;
|
|
4183
4315
|
// The whole conversation stays reachable: the cap is only a safety valve (see
|
|
4184
4316
|
// SCROLLBACK_BLOCKS). Blocks outside the window cost one number each here and nothing
|
|
4185
4317
|
// in Yoga, which only ever sees the window and its spacers.
|
|
@@ -4281,10 +4413,14 @@ export function App({ resumeSessionId, initialScreen }) {
|
|
|
4281
4413
|
// The status line, the queued bar, the input and whatever sits under it. Built once
|
|
4282
4414
|
// and rendered by BOTH shells: the only thing that differs between them is where it
|
|
4283
4415
|
// ends up — pinned to the bottom of a frame we own, or simply the last thing printed.
|
|
4284
|
-
const footerView = (_jsxs(Box, { ref: footerRef, flexDirection: "column", flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { flexShrink: 0,
|
|
4416
|
+
const footerView = (_jsxs(Box, { ref: footerRef, flexDirection: "column", flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { flexShrink: 0, marginBottom: queued.length + departing.length > 0 ? 1 : 0, ref: (node) => {
|
|
4417
|
+
queueRow.current = node && queued.length + departing.length > 0 ? measureElement(node).y : null;
|
|
4418
|
+
}, children: _jsx(QueuedBar, { queued: [...departing, ...queued] }) }), _jsx(Box, { flexShrink: 0, children: _jsx(StatusLine, { busy: busy, userCommand: busy && userTurn.current, startedAt: turnStart.current, lastMs: lastMs, usage: taskUsage, received: liveTokens, advance: advanceTokens, waiting: overlay?.kind === "approval", waited: () => waitedMs.current + (waitSince.current != null ? Date.now() - waitSince.current : 0) }) }), marathonUi ? (_jsx(Box, { flexShrink: 0, children: _jsx(MarathonBox, { ui: marathonUi, width: width, cap: shell === "inline" ? 4 : 7 }) })) : null, _jsx(Box, { flexShrink: 0, flexDirection: "column", children: ready ? (_jsx(PromptInput, { onSubmit: onSend, onForceSend: forceSend, opening: opening, disabled: false, placeholder: marathonUi?.phase === "armed" ? "describe the goal…" : busy ? "type to queue a message…" : "say something…", width: width, history: history, completions: completions, pathComplete: pathComplete.current, onLargePaste: registerPaste, onDroppedPaths: (text) => dropHandles.current.register(text), registerCaretClick: (place) => {
|
|
4285
4419
|
caretClick.current = place;
|
|
4286
4420
|
}, registerTextSelect: (select) => {
|
|
4287
4421
|
textSelect.current = select;
|
|
4422
|
+
}, registerQueuePull: (pull) => {
|
|
4423
|
+
pullQueue.current = pull;
|
|
4288
4424
|
}, maxMenuRows: maxMenuItems, menuAbove: shell === "inline", settleKey: committed.length, onMenuChange: onMenuChange, placeCursor: shell === "inline", onQueuePop: popQueue, overlay: overlayView, ...(searchPlaceholder ? { searchPlaceholder } : {}), fill: fill })) : (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "starting\u2026" }) })) }), ctxWarn ? (
|
|
4289
4425
|
// A compaction is near. This takes the line over the tip and the background bar:
|
|
4290
4426
|
// it is the one thing here the user needs BEFORE it happens, since the summarizing
|
|
@@ -4789,16 +4925,27 @@ function StatusLine({ busy, userCommand, startedAt, lastMs, usage, received, adv
|
|
|
4789
4925
|
// rather than as jumping, and the working dot moves smoothly. It was 50ms (20 frames a
|
|
4790
4926
|
// second), which was the slowest-looking thing on a screen that could draw three times as
|
|
4791
4927
|
// often, and at 1Hz before that it moved once a second in whatever lump had arrived, which
|
|
4792
|
-
// is a stutter, not an animation.
|
|
4793
|
-
//
|
|
4794
|
-
//
|
|
4795
|
-
//
|
|
4928
|
+
// is a stutter, not an animation. Only the easing advances on every tick; a render is asked for
|
|
4929
|
+
// only when the line's text would change (see below), because a tick that draws the same text
|
|
4930
|
+
// still costs a whole pass through layout and the terminal writer, about a millisecond each,
|
|
4931
|
+
// sixty times a second, for the whole time the agent works.
|
|
4796
4932
|
const [, tick] = useState(0);
|
|
4933
|
+
// The line as the last render drew it, and the line as it would be drawn now. The clock below runs
|
|
4934
|
+
// at the display's pace to keep the counter easing smoothly, but a render that would draw the same
|
|
4935
|
+
// text is a whole pass through layout and the terminal writer for nothing, and most ticks are that:
|
|
4936
|
+
// the seconds change once a second and the count only when the eased figure moves a whole step.
|
|
4937
|
+
// Read through a ref so the interval never has to be torn down when the props change.
|
|
4938
|
+
const drawn = useRef("");
|
|
4939
|
+
const nowKey = useRef(() => "");
|
|
4940
|
+
nowKey.current = () => (busy && startedAt != null ? statusKey(userCommand, waiting, startedAt, waited(), received()) : "");
|
|
4941
|
+
drawn.current = nowKey.current();
|
|
4797
4942
|
useEffect(() => {
|
|
4798
4943
|
if (!busy)
|
|
4799
4944
|
return;
|
|
4800
4945
|
const id = setInterval(() => {
|
|
4801
4946
|
advance();
|
|
4947
|
+
if (nowKey.current() === drawn.current)
|
|
4948
|
+
return;
|
|
4802
4949
|
tick((t) => t + 1);
|
|
4803
4950
|
}, LIVE_TICK_MS);
|
|
4804
4951
|
return () => clearInterval(id);
|
|
@@ -4849,6 +4996,16 @@ const DOUBLE_CLICK_MS = 500;
|
|
|
4849
4996
|
const EDGE_SCROLL_MS = 45;
|
|
4850
4997
|
/** The working line's render clock, in milliseconds. See StatusLine's tick. */
|
|
4851
4998
|
const LIVE_TICK_MS = 16;
|
|
4999
|
+
/**
|
|
5000
|
+
* Everything the working line's text is made of, as one string (pure). Two equal keys draw the same
|
|
5001
|
+
* line, so a render for an unchanged key can be skipped. Built from the same calls the line uses.
|
|
5002
|
+
*/
|
|
5003
|
+
export function statusKey(userCommand, waiting, startedAt, waitedMs, got) {
|
|
5004
|
+
const secs = workedSeconds(Date.now(), startedAt, waitedMs);
|
|
5005
|
+
if (userCommand)
|
|
5006
|
+
return `u|${fmtElapsed(secs)}`;
|
|
5007
|
+
return `${waiting ? "w" : workingVerb(startedAt, Date.now() - startedAt - waitedMs)}|${fmtElapsed(secs)}|${got > 0 ? formatTokens(got) : ""}`;
|
|
5008
|
+
}
|
|
4852
5009
|
function fmtElapsed(s) {
|
|
4853
5010
|
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
4854
5011
|
}
|
|
@@ -4914,11 +5071,18 @@ function bgClock(ms) {
|
|
|
4914
5071
|
* you cannot see the handle on — ↑ is not a thing anyone guesses, and the cost of not
|
|
4915
5072
|
* guessing it is a message you no longer wanted being sent anyway.
|
|
4916
5073
|
*/
|
|
5074
|
+
/**
|
|
5075
|
+
* What is waiting to be sent, on ONE line shaped like a work row: the marker, what it is, a dot
|
|
5076
|
+
* between the parts, the key that sends it, and the arrow that opens it (a click pulls the queue back
|
|
5077
|
+
* into the box to edit, as the up key does). Long text is cut so the key and the arrow always stay
|
|
5078
|
+
* on the line.
|
|
5079
|
+
*/
|
|
4917
5080
|
function QueuedBar({ queued }) {
|
|
4918
5081
|
if (queued.length === 0)
|
|
4919
5082
|
return null;
|
|
4920
|
-
const
|
|
4921
|
-
|
|
5083
|
+
const first = queued[0].text.replace(/\s+/g, " ").trim();
|
|
5084
|
+
const more = queued.length - 1;
|
|
5085
|
+
return (_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { dimColor: true, children: "● " }), _jsx(Box, { flexShrink: 1, children: _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["queued: ", first] }) }), _jsx(Box, { flexShrink: 0, children: _jsxs(Text, { dimColor: true, children: [more > 0 ? ` · ${more} more` : "", " · ctrl+F to send now ", "▸"] }) })] }));
|
|
4922
5086
|
}
|
|
4923
5087
|
/**
|
|
4924
5088
|
* Whether some OTHER installed provider has a key, so `/provider` is worth
|