zidane 6.2.13 → 6.2.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/dist/acp-cli.js +2 -2
- package/dist/{anthropic-BYsc6qWj.js → anthropic-CTAXBVsj.js} +2 -2
- package/dist/anthropic-CTAXBVsj.js.map +1 -0
- package/dist/{auth-Bb479DFB.js → auth-BCpEbzvO.js} +2 -2
- package/dist/{auth-Bb479DFB.js.map → auth-BCpEbzvO.js.map} +1 -1
- package/dist/chat/pure.d.ts +3 -3
- package/dist/chat/pure.js +3 -2
- package/dist/chat.d.ts +3 -3
- package/dist/chat.js +5 -4
- package/dist/chat.js.map +1 -1
- package/dist/history-search-C4bk1Wz_.js +486 -0
- package/dist/history-search-C4bk1Wz_.js.map +1 -0
- package/dist/history-search-worker.d.ts +1 -0
- package/dist/history-search-worker.js +86 -0
- package/dist/history-search-worker.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers.js +1 -1
- package/dist/{tool-formatters-BSwVkSX2.d.ts → tool-formatters-DdgS-xE2.d.ts} +90 -4
- package/dist/tool-formatters-DdgS-xE2.d.ts.map +1 -0
- package/dist/transcript-anchors-DKjXpQ0h.d.ts.map +1 -1
- package/dist/{transcript-anchors-yogJHuBW.js → transcript-anchors-DZ-8BF4p.js} +3 -3
- package/dist/transcript-anchors-DZ-8BF4p.js.map +1 -0
- package/dist/tui.d.ts +52 -10
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +1340 -246
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-CXjRaltC.js → turn-operations-CokV32wf.js} +82 -11
- package/dist/turn-operations-CokV32wf.js.map +1 -0
- package/dist/{turn-operations-CjkgQKmR.d.ts → turn-operations-sE-Sli9H.d.ts} +2 -2
- package/dist/{turn-operations-CjkgQKmR.d.ts.map → turn-operations-sE-Sli9H.d.ts.map} +1 -1
- package/package.json +1 -1
- package/dist/anthropic-BYsc6qWj.js.map +0 -1
- package/dist/tool-formatters-BSwVkSX2.d.ts.map +0 -1
- package/dist/transcript-anchors-yogJHuBW.js.map +0 -1
- package/dist/turn-operations-CXjRaltC.js.map +0 -1
package/dist/tui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as useMcpAuthState, $n as buildPlanSystem, $t as restoreChainFallbacks, A as runOAuthLogin, Bt as createDiscoverySlot, Ct as SETTINGS_CHOICES, D as formatPathForCwd, Dt as clampFps, Et as SettingsProvider, G as buildVisibleMcpRows, Gt as useConfig, Ht as useDiscovery, J as createFileMcpCredentialStore, Jt as buildRunModel, K as indexOfServerRow, Kt as resolveConfig, L as loadMcpToolsCache, M as buildMcpServers, Mt as resolveTheme, O as fetchOAuthRedirect, Ot as useSettings, P as discoverProjectMcps, Q as useMcpAuthDispatch, Qn as buildBuildSystem, Qt as providerKeySetChanged, R as mcpToolsCachePath, St as SETTINGS_CATEGORIES, T as suggestSafelistEntries, Tn as accentColor, Tt as SETTINGS_TOGGLES, U as useMcpToolToggleMap, Ut as useDiscoveryOptional, V as subscribeMcpToolsCache, Vn as useActiveTodos, Vt as DiscoveryProvider, Wt as ConfigProvider, Xt as chainsEqual, Yt as chainPositionOf, Z as McpAuthProvider, Zt as filterChainFallbacks, _ as useSafeModeQueue, _n as discoverAgentsMd, _r as toolCallPreview, _t as buildHints, a as useSurfaces, an as bootTick, at as buildResumedToolResultsTurn, b as getSafelist, br as usageByModel, bt as useEnabledToggleSet, c as useStreamBuffer, cr as lastContextSizeFromTurns, ct as makeRequestInteraction, d as discoverProjectSkills, dr as marginTopFor, dt as useInteractionsActions, er as envSection, et as getMcpAuthStatus, ft as useInteractionsQueue, g as useSafeModeActions, gn as chatAutoCompactBehavior, gt as truncateTrailing, h as SafeModeProvider, hr as sumRunCosts, ht as hintsLength, i as useSelectStyle, ir as compactSummaryEvent, it as PRESENT_PLAN_TOOL, j as supportsOAuth, jn as TODO_STATUS_GLYPHS, jt as resolveChipColor, k as oauthUsesManualCodePaste, l as buildSkillsConfig, lr as listSessionMeta, lt as pendingInteractionsFromTurns, m as writeSessionExport, mr as stripSpawnTokensLine, mt as clipHintsToWidth, n as ThemeProvider, nn as useCompletion, nr as createChatTurnsReadTool, nt as ASK_USER_TOOL, on as buildUpdateHint, or as deriveSessionTitle, ot as createInteractionTools, pr as serverToolResultSummary, pt as EMPTY_HINTS, q as parentServerName, qt as resolveStoragePaths, r as useColors, rn as tryOpenBrowser, rt as InteractionsProvider, s as useTheme, sn as useUpdateCheck, sr as eventsFromTurns, t as computeTurnAnchors, tn as toggleChainEntry, tr as chatReferenceTokensForPrompt, u as defaultSkillScanPaths, ur as loadState, vr as toolResultText, wt as SETTINGS_INPUTS, x as isOnSafelist, xt as DEFAULT_SETTINGS, y as addToSafelist, yr as updateToolEventOutcomes, yt as listProjectFiles, z as refreshMcpToolsCatalog } from "./transcript-anchors-
|
|
1
|
+
import { $ as useMcpAuthState, $n as buildPlanSystem, $t as restoreChainFallbacks, A as runOAuthLogin, Bt as createDiscoverySlot, Ct as SETTINGS_CHOICES, D as formatPathForCwd, Dt as clampFps, Et as SettingsProvider, G as buildVisibleMcpRows, Gt as useConfig, Ht as useDiscovery, J as createFileMcpCredentialStore, Jt as buildRunModel, K as indexOfServerRow, Kt as resolveConfig, L as loadMcpToolsCache, M as buildMcpServers, Mt as resolveTheme, O as fetchOAuthRedirect, Ot as useSettings, P as discoverProjectMcps, Q as useMcpAuthDispatch, Qn as buildBuildSystem, Qt as providerKeySetChanged, R as mcpToolsCachePath, St as SETTINGS_CATEGORIES, T as suggestSafelistEntries, Tn as accentColor, Tt as SETTINGS_TOGGLES, U as useMcpToolToggleMap, Ut as useDiscoveryOptional, V as subscribeMcpToolsCache, Vn as useActiveTodos, Vt as DiscoveryProvider, Wt as ConfigProvider, Xt as chainsEqual, Yt as chainPositionOf, Z as McpAuthProvider, Zt as filterChainFallbacks, _ as useSafeModeQueue, _n as discoverAgentsMd, _r as toolCallPreview, _t as buildHints, a as useSurfaces, an as bootTick, at as buildResumedToolResultsTurn, b as getSafelist, br as usageByModel, bt as useEnabledToggleSet, c as useStreamBuffer, cr as lastContextSizeFromTurns, ct as makeRequestInteraction, d as discoverProjectSkills, dr as marginTopFor, dt as useInteractionsActions, er as envSection, et as getMcpAuthStatus, ft as useInteractionsQueue, g as useSafeModeActions, gn as chatAutoCompactBehavior, gt as truncateTrailing, h as SafeModeProvider, hr as sumRunCosts, ht as hintsLength, i as useSelectStyle, ir as compactSummaryEvent, it as PRESENT_PLAN_TOOL, j as supportsOAuth, jn as TODO_STATUS_GLYPHS, jt as resolveChipColor, k as oauthUsesManualCodePaste, l as buildSkillsConfig, lr as listSessionMeta, lt as pendingInteractionsFromTurns, m as writeSessionExport, mr as stripSpawnTokensLine, mt as clipHintsToWidth, n as ThemeProvider, nn as useCompletion, nr as createChatTurnsReadTool, nt as ASK_USER_TOOL, on as buildUpdateHint, or as deriveSessionTitle, ot as createInteractionTools, pr as serverToolResultSummary, pt as EMPTY_HINTS, q as parentServerName, qt as resolveStoragePaths, r as useColors, rn as tryOpenBrowser, rt as InteractionsProvider, s as useTheme, sn as useUpdateCheck, sr as eventsFromTurns, t as computeTurnAnchors, tn as toggleChainEntry, tr as chatReferenceTokensForPrompt, u as defaultSkillScanPaths, ur as loadState, vr as toolResultText, wt as SETTINGS_INPUTS, x as isOnSafelist, xt as DEFAULT_SETTINGS, y as addToSafelist, yr as updateToolEventOutcomes, yt as listProjectFiles, z as refreshMcpToolsCatalog } from "./transcript-anchors-DZ-8BF4p.js";
|
|
2
2
|
import { $ as composeExtensionPreset, B as ExtensionInstallError, Bt as ensureKeybindingsFile, Et as createModelSearchTool, H as installExtensionFromSpec, Ht as groupBindings, J as createExtensionHud, K as removeInstalledExtension, P as summaryToTurn, St as tailTruncate, Ut as keybindingsPath, Vt as formatBindingForDisplay, Wt as matchesBinding, X as discoverAndBuildExtensionRegistry, Y as registerExtensionHostModules, Yt as resolveExtensionKeybindings, _ as buildPostCompactAttachments, c as applyCompactSummaryCutoff, dn as sanitizeExtensionConfigActionPatch, en as inlineLoadedExtension, et as disposeExtensionRegistry, f as cleanupPersistedSession, g as resolveTasksDir, h as resolvePersistDir, hn as effectiveContextWindow, ht as isSpawnTool, it as createExtensionHostRuntimeBridge, lt as basic_default, m as resolveMcpWarningsDir, mn as OUTPUT_RESERVE_TOKENS, nn as parseExtensionScanPathEnv, on as coerceExtensionConfigInput, q as updateInstalledExtension, rn as scanExtensionDirectories, rt as partitionExtensionCatalog, t as createAgent, tt as filterExtensionRegistry, un as resolveExtensionConfigValue, wt as looksBinary, x as compactConversation, y as selectFilesFromSession, z as createExtensionPromptBridge, zt as KEYBINDING_KEY_COL_WIDTH } from "./agent-DoEx_WD3.js";
|
|
3
3
|
import { a as formatDuration, c as previewLine, i as fmtTokens, l as shortId, n as compactPath, o as formatTaskStatus, r as fmtCost, s as formatTaskSummary, t as ageString } from "./format-BF745pLL.js";
|
|
4
4
|
import { n as appendCapped } from "./output-cap-kBUHBlnq.js";
|
|
@@ -16,10 +16,11 @@ import { a as generateStructuredData, c as shouldAutoGenerateTitle, f as command
|
|
|
16
16
|
import { s as resolveCompletionSubmit } from "./completion-core-BRo86R_L.js";
|
|
17
17
|
import { i as installSkillFromSpec, r as SkillInstallError } from "./skills-DKWqoaYH.js";
|
|
18
18
|
import { i as ZIDANE_EXTENSION_SKILL_NAME, l as withZidaneExtensionSkill, o as extensionDirForCwd, r as EXTENSION_EDIT_METADATA_KEY, s as extensionScaffold, t as loginMcpServer } from "./login-Clf-R27F.js";
|
|
19
|
-
import { C as restoreModelOptions, S as piIdOf, _ as modelOptionsFor, m as getContextWindow, p as enabledModelOptions, s as setProviderCredential, t as detectAuth, v as modelSupportsReasoning } from "./auth-
|
|
19
|
+
import { C as restoreModelOptions, S as piIdOf, _ as modelOptionsFor, m as getContextWindow, p as enabledModelOptions, s as setProviderCredential, t as detectAuth, v as modelSupportsReasoning } from "./auth-BCpEbzvO.js";
|
|
20
20
|
import { createDockerContext } from "./contexts/docker.js";
|
|
21
21
|
import { createTuiStore } from "./session/sqlite.js";
|
|
22
|
-
import { $ as
|
|
22
|
+
import { $ as extractEditPayload, B as isVisible, C as chunkMarkdownForRender, E as stripHtmlCommentsOutsideCode, F as BASH_CALL_ID_PREFIX, G as streamEventSearchText, H as promptHistoryFromEvents, I as EDIT_TOOL_NAMES, J as buildContextualDiff, K as turnSelectionOwnership, L as isEditErrorResult, N as blendHsl, O as createSkillsCompletionProvider, P as buildLinearRamp, R as isTurnHighlighted, T as splitMarkdownCodeBlocks, U as selectableTurnIds, V as isVisibleDuringSearch, W as selectionAfterTurnRemoval, X as clipDiffForRender, Y as buildUnifiedDiff, _ as turnContextSize, a as TOOL_DISPLAY, at as buildEditOutcomesAnnotation, b as filterModelCatalog, ct as parseEditOutcomesFromResult, d as finalizeStreamingMarkdown, dt as stripEditOutcomesAnnotation, et as filetypeFromPath, f as finalizeStreamingMarkdownForOwner, ft as summarizeOutcomes, g as revealEarlierTranscriptEvents, h as replaceTranscriptTurnEvents, ht as createChatsCompletionProvider, i as turnAsText, j as createFilesCompletionProvider, k as uniqueSkillNamesFromReferences, lt as resolveApprovalForPayload, n as deleteTurnSafely, o as displayNameFor, p as hiddenEarlierTranscriptCount, r as truncateTurnsAt, rt as summarizeEditPayload, s as formatToolCall, st as mergeApprovalAndBodyOutcomes, tt as previewEditPayload, u as capTranscriptEvents, ut as rewriteMultiEditHeader, v as splitPromptSegments, w as normalizeMarkdownCodeFences, y as buildModelCatalog, z as isUserBashEvent } from "./turn-operations-CokV32wf.js";
|
|
23
|
+
import { a as searchHighlightRanges, c as searchSessionHistoryIndexAsync, i as highlightSegments, n as buildSessionSearchIndexAsync, r as filterSessionSearchCatalog, u as searchSessionIndexAsync } from "./history-search-C4bk1Wz_.js";
|
|
23
24
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
24
25
|
import { Buffer } from "node:buffer";
|
|
25
26
|
import * as fs from "node:fs";
|
|
@@ -1505,15 +1506,16 @@ const BASH_RESULT_MAX_LINES = 10;
|
|
|
1505
1506
|
* same-turn events read as one continuous highlighted block instead of a
|
|
1506
1507
|
* striped list.
|
|
1507
1508
|
*/
|
|
1508
|
-
const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, anchorId, hideChildLabel = false, settleCodeEpoch = 0, liveSpawn }) => {
|
|
1509
|
+
const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, anchorId, hideChildLabel = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false }) => {
|
|
1509
1510
|
const SURFACE = useSurfaces();
|
|
1510
1511
|
const gap = marginTopFor(event, previous);
|
|
1512
|
+
const highlighted = selected;
|
|
1511
1513
|
return /* @__PURE__ */ jsx$1("box", {
|
|
1512
1514
|
id: anchorId,
|
|
1513
1515
|
style: {
|
|
1514
|
-
marginTop:
|
|
1515
|
-
paddingTop:
|
|
1516
|
-
backgroundColor:
|
|
1516
|
+
marginTop: 0,
|
|
1517
|
+
paddingTop: gap,
|
|
1518
|
+
backgroundColor: highlighted ? SURFACE.selection : SURFACE.background,
|
|
1517
1519
|
alignSelf: "stretch",
|
|
1518
1520
|
flexShrink: 0,
|
|
1519
1521
|
flexDirection: "column"
|
|
@@ -1523,9 +1525,11 @@ const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, an
|
|
|
1523
1525
|
previous,
|
|
1524
1526
|
depthOffset,
|
|
1525
1527
|
hideChildLabel,
|
|
1526
|
-
selected,
|
|
1528
|
+
selected: highlighted,
|
|
1527
1529
|
settleCodeEpoch,
|
|
1528
|
-
liveSpawn
|
|
1530
|
+
liveSpawn,
|
|
1531
|
+
searchQuery,
|
|
1532
|
+
searchTitleMatch
|
|
1529
1533
|
})
|
|
1530
1534
|
});
|
|
1531
1535
|
});
|
|
@@ -1558,8 +1562,8 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
|
|
|
1558
1562
|
const timer = setInterval(() => setCompactTick((t) => t + 1), 1e3);
|
|
1559
1563
|
return () => clearInterval(timer);
|
|
1560
1564
|
}, [status]);
|
|
1561
|
-
const
|
|
1562
|
-
const sW = status === "compacting" ? 2 +
|
|
1565
|
+
const statusLabel = status === "compacting" ? compactingLabel(compactionTokens, compactionStartedAt) : status === "searching" ? " Searching messages…" : "";
|
|
1566
|
+
const sW = status === "compacting" || status === "searching" ? 2 + statusLabel.length : status === "asking" ? 3 : status ? 2 : 0;
|
|
1563
1567
|
if (hW + sW + (rightW > 0 ? rightW + 1 : 0) <= inner) return /* @__PURE__ */ jsxs("box", {
|
|
1564
1568
|
style: {
|
|
1565
1569
|
flexDirection: "row",
|
|
@@ -1571,7 +1575,7 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
|
|
|
1571
1575
|
/* @__PURE__ */ jsx$1(HintsText, { hints }),
|
|
1572
1576
|
/* @__PURE__ */ jsx$1(FooterStatusIcon, {
|
|
1573
1577
|
status,
|
|
1574
|
-
|
|
1578
|
+
label: statusLabel
|
|
1575
1579
|
}),
|
|
1576
1580
|
/* @__PURE__ */ jsx$1("box", { style: { flexGrow: 1 } }),
|
|
1577
1581
|
/* @__PURE__ */ jsx$1(RightStatus, {
|
|
@@ -1594,7 +1598,7 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
|
|
|
1594
1598
|
},
|
|
1595
1599
|
children: [/* @__PURE__ */ jsx$1(HintsText, { hints: stackedHints }), /* @__PURE__ */ jsx$1(FooterStatusIcon, {
|
|
1596
1600
|
status,
|
|
1597
|
-
|
|
1601
|
+
label: statusLabel
|
|
1598
1602
|
})]
|
|
1599
1603
|
}), rightW > 0 && /* @__PURE__ */ jsxs("box", {
|
|
1600
1604
|
style: {
|
|
@@ -1625,16 +1629,16 @@ function RightStatus({ cost, context }) {
|
|
|
1625
1629
|
* compacting adds a short label because background compaction can otherwise
|
|
1626
1630
|
* look like an idle spinner.
|
|
1627
1631
|
*/
|
|
1628
|
-
function FooterStatusIcon({ status,
|
|
1632
|
+
function FooterStatusIcon({ status, label = "" }) {
|
|
1629
1633
|
const COLOR = useColors();
|
|
1630
1634
|
if (!status) return null;
|
|
1631
1635
|
const glyph = status === "asking" ? /* @__PURE__ */ jsx$1("span", { children: "❓" }) : status === "busy" ? /* @__PURE__ */ jsx$1(StatusSpinner, { color: COLOR.warn }) : /* @__PURE__ */ jsx$1(StatusSpinner, { color: COLOR.accent });
|
|
1632
1636
|
return /* @__PURE__ */ jsxs("text", { children: [
|
|
1633
1637
|
/* @__PURE__ */ jsx$1("span", { children: " " }),
|
|
1634
1638
|
glyph,
|
|
1635
|
-
|
|
1639
|
+
label && /* @__PURE__ */ jsx$1("span", {
|
|
1636
1640
|
fg: COLOR.accent,
|
|
1637
|
-
children:
|
|
1641
|
+
children: label
|
|
1638
1642
|
})
|
|
1639
1643
|
] });
|
|
1640
1644
|
}
|
|
@@ -1924,9 +1928,8 @@ function StatusSpinner({ color }) {
|
|
|
1924
1928
|
children: useSpinnerFrame()
|
|
1925
1929
|
});
|
|
1926
1930
|
}
|
|
1927
|
-
function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
|
|
1931
|
+
function Transcript({ events, settings, selectedTurnId = null, busy = false, searchQuery, searchPending = false, onLoadEarlier }) {
|
|
1928
1932
|
const COLOR = useColors();
|
|
1929
|
-
const items = useMemo(() => partitionTranscript(events, settings), [events, settings]);
|
|
1930
1933
|
const showThrobber = busy && settings.showThrobber;
|
|
1931
1934
|
const codeSettle = useRef({
|
|
1932
1935
|
wasBusy: false,
|
|
@@ -1949,8 +1952,48 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
|
|
|
1949
1952
|
codeSettle.current.wasBusy = busy;
|
|
1950
1953
|
const throbberLabel = events.length > 0 && events[events.length - 1].kind === "thinking" ? "Thinking" : void 0;
|
|
1951
1954
|
const ownership = useMemo(() => turnSelectionOwnership(events), [events]);
|
|
1955
|
+
const searchMatches = useMemo(() => {
|
|
1956
|
+
const direct = /* @__PURE__ */ new Set();
|
|
1957
|
+
const callIds = /* @__PURE__ */ new Set();
|
|
1958
|
+
const childIds = /* @__PURE__ */ new Set();
|
|
1959
|
+
if (!searchQuery) return {
|
|
1960
|
+
direct,
|
|
1961
|
+
callIds,
|
|
1962
|
+
childIds
|
|
1963
|
+
};
|
|
1964
|
+
const stack = [];
|
|
1965
|
+
for (const event of events) {
|
|
1966
|
+
if (event.kind === "spawn-start") stack.push(event);
|
|
1967
|
+
if (searchHighlightRanges(streamEventSearchText(event), searchQuery).length > 0) {
|
|
1968
|
+
direct.add(event);
|
|
1969
|
+
if (event.callId) callIds.add(event.callId);
|
|
1970
|
+
if ((event.depth ?? 0) > 0) {
|
|
1971
|
+
for (const parent of stack) if (parent.childId) childIds.add(parent.childId);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
if (event.kind === "spawn-end") {
|
|
1975
|
+
const index = stack.findLastIndex((parent) => parent.childId === event.childId);
|
|
1976
|
+
if (index >= 0) stack.splice(index);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
return {
|
|
1980
|
+
direct,
|
|
1981
|
+
callIds,
|
|
1982
|
+
childIds
|
|
1983
|
+
};
|
|
1984
|
+
}, [events, searchQuery]);
|
|
1985
|
+
const items = useMemo(() => partitionTranscript(events, settings, searchQuery ? (event) => searchMatches.direct.has(event) : void 0), [
|
|
1986
|
+
events,
|
|
1987
|
+
settings,
|
|
1988
|
+
searchQuery,
|
|
1989
|
+
searchMatches
|
|
1990
|
+
]);
|
|
1952
1991
|
const scrollboxRef = useRef(null);
|
|
1953
1992
|
const rowVisibility = useRef(createRowVisibilityStore());
|
|
1993
|
+
const loadEarlierRef = useRef(onLoadEarlier);
|
|
1994
|
+
loadEarlierRef.current = onLoadEarlier;
|
|
1995
|
+
const earlierLoad = useRef(null);
|
|
1996
|
+
const earlierLoadArmed = useRef(false);
|
|
1954
1997
|
useEffect(() => {
|
|
1955
1998
|
const timer = setInterval(() => {
|
|
1956
1999
|
const sb = scrollboxRef.current;
|
|
@@ -1959,9 +2002,45 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
|
|
|
1959
2002
|
top: sb.viewport.y,
|
|
1960
2003
|
height: sb.viewport.height
|
|
1961
2004
|
});
|
|
2005
|
+
const topThreshold = Math.max(4, Math.floor(sb.viewport.height / 4));
|
|
2006
|
+
if (sb.scrollTop > topThreshold) earlierLoadArmed.current = true;
|
|
2007
|
+
else if (earlierLoadArmed.current && !earlierLoad.current && loadEarlierRef.current && hiddenEarlierTranscriptCount(events) > 0) {
|
|
2008
|
+
earlierLoadArmed.current = false;
|
|
2009
|
+
const snapshot = {
|
|
2010
|
+
oldHeight: sb.scrollHeight,
|
|
2011
|
+
oldTop: sb.scrollTop
|
|
2012
|
+
};
|
|
2013
|
+
earlierLoad.current = snapshot;
|
|
2014
|
+
if (!loadEarlierRef.current()) {
|
|
2015
|
+
earlierLoad.current = null;
|
|
2016
|
+
earlierLoadArmed.current = true;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
1962
2019
|
}, 200);
|
|
1963
2020
|
return () => clearInterval(timer);
|
|
1964
|
-
}, []);
|
|
2021
|
+
}, [events]);
|
|
2022
|
+
useEffect(() => {
|
|
2023
|
+
const pending = earlierLoad.current;
|
|
2024
|
+
if (!pending) return;
|
|
2025
|
+
let cancelled = false;
|
|
2026
|
+
let attempts = 0;
|
|
2027
|
+
const restore = () => {
|
|
2028
|
+
if (cancelled) return;
|
|
2029
|
+
const sb = scrollboxRef.current;
|
|
2030
|
+
if (attempts < 5 && (!sb || sb.scrollHeight <= pending.oldHeight)) {
|
|
2031
|
+
attempts++;
|
|
2032
|
+
requestAnimationFrame(restore);
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
if (sb) sb.scrollTop = pending.oldTop + Math.max(0, sb.scrollHeight - pending.oldHeight);
|
|
2036
|
+
earlierLoad.current = null;
|
|
2037
|
+
};
|
|
2038
|
+
const handle = requestAnimationFrame(restore);
|
|
2039
|
+
return () => {
|
|
2040
|
+
cancelled = true;
|
|
2041
|
+
cancelAnimationFrame(handle);
|
|
2042
|
+
};
|
|
2043
|
+
}, [events]);
|
|
1965
2044
|
const modal = useModalState();
|
|
1966
2045
|
useKeyboard(useCallback((key) => {
|
|
1967
2046
|
if (modal?.isOpen) return;
|
|
@@ -1993,36 +2072,56 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
|
|
|
1993
2072
|
}, [items, events]);
|
|
1994
2073
|
useEffect(() => {
|
|
1995
2074
|
if (!selectedTurnId) return;
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
const
|
|
2075
|
+
let handle = 0;
|
|
2076
|
+
let attempts = 0;
|
|
2077
|
+
const align = () => {
|
|
2078
|
+
const scrollbox = scrollboxRef.current;
|
|
2079
|
+
if (!scrollbox) return;
|
|
2080
|
+
const id = anchors.idByTurn.get(selectedTurnId);
|
|
2081
|
+
if (searchQuery && id) {
|
|
2082
|
+
const child = scrollbox.content.findDescendantById(id);
|
|
2083
|
+
if (child && scrollbox.scrollHeight > scrollbox.viewport.height) {
|
|
2084
|
+
scrollbox.scrollTop = child.y - scrollbox.content.y;
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
if (attempts++ < 5) handle = requestAnimationFrame(align);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
1999
2090
|
const ownsLast = anchors.lastTurnId !== void 0 && ownership.get(anchors.lastTurnId) === selectedTurnId;
|
|
2000
2091
|
if (selectedTurnId === anchors.lastTurnId || ownsLast) {
|
|
2001
2092
|
scrollbox.scrollTop = scrollbox.scrollHeight;
|
|
2002
2093
|
return;
|
|
2003
2094
|
}
|
|
2004
|
-
const id = anchors.idByTurn.get(selectedTurnId);
|
|
2005
2095
|
if (id) scrollbox.scrollChildIntoView(id);
|
|
2006
|
-
}
|
|
2096
|
+
};
|
|
2097
|
+
handle = requestAnimationFrame(align);
|
|
2007
2098
|
return () => cancelAnimationFrame(handle);
|
|
2008
2099
|
}, [
|
|
2009
2100
|
selectedTurnId,
|
|
2010
2101
|
anchors,
|
|
2011
|
-
ownership
|
|
2102
|
+
ownership,
|
|
2103
|
+
searchQuery
|
|
2012
2104
|
]);
|
|
2013
|
-
if (items.length === 0 && !showThrobber)
|
|
2105
|
+
if (items.length === 0 && !showThrobber) {
|
|
2106
|
+
if (searchQuery) return searchPending ? /* @__PURE__ */ jsx$1("box", { style: { flexGrow: 1 } }) : /* @__PURE__ */ jsx$1("text", {
|
|
2107
|
+
fg: COLOR.mute,
|
|
2108
|
+
children: `No messages match “${searchQuery.trim()}”`
|
|
2109
|
+
});
|
|
2110
|
+
return /* @__PURE__ */ jsx$1(EmptyState, {});
|
|
2111
|
+
}
|
|
2014
2112
|
return /* @__PURE__ */ jsx$1(RowVisibilityContext.Provider, {
|
|
2015
2113
|
value: rowVisibility.current.store,
|
|
2016
2114
|
children: /* @__PURE__ */ jsxs("scrollbox", {
|
|
2017
2115
|
ref: scrollboxRef,
|
|
2116
|
+
id: "transcript-scrollbox",
|
|
2018
2117
|
focusable: false,
|
|
2019
2118
|
style: {
|
|
2020
2119
|
flexGrow: 1,
|
|
2021
2120
|
paddingLeft: 1,
|
|
2022
2121
|
paddingRight: 1
|
|
2023
2122
|
},
|
|
2024
|
-
stickyScroll:
|
|
2025
|
-
stickyStart: "bottom",
|
|
2123
|
+
stickyScroll: !searchQuery,
|
|
2124
|
+
stickyStart: searchQuery ? void 0 : "bottom",
|
|
2026
2125
|
verticalScrollbarOptions: {
|
|
2027
2126
|
width: 1,
|
|
2028
2127
|
trackOptions: {
|
|
@@ -2039,7 +2138,9 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
|
|
|
2039
2138
|
selected: isTurnHighlighted(item.event, selectedTurnId, ownership),
|
|
2040
2139
|
anchorId: anchors.ids[i][0],
|
|
2041
2140
|
settleCodeEpoch: codeSettle.current.targets.has(item.event) ? codeSettle.current.epoch : 0,
|
|
2042
|
-
liveSpawn: live?.[0]
|
|
2141
|
+
liveSpawn: live?.[0],
|
|
2142
|
+
searchQuery,
|
|
2143
|
+
searchTitleMatch: item.event.kind === "tool" && item.event.callId !== void 0 && searchMatches.callIds.has(item.event.callId) || (item.event.kind === "spawn-start" || item.event.kind === "spawn-end") && item.event.childId !== void 0 && searchMatches.childIds.has(item.event.childId)
|
|
2043
2144
|
}, i);
|
|
2044
2145
|
}
|
|
2045
2146
|
return /* @__PURE__ */ jsx$1(SubagentBlock, {
|
|
@@ -2049,7 +2150,9 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
|
|
|
2049
2150
|
anchorIds: anchors.ids[i],
|
|
2050
2151
|
codeSettleTargets: codeSettle.current.targets,
|
|
2051
2152
|
codeSettleEpoch: codeSettle.current.epoch,
|
|
2052
|
-
live: liveByItem.get(i)
|
|
2153
|
+
live: liveByItem.get(i),
|
|
2154
|
+
searchQuery,
|
|
2155
|
+
searchTitleMatches: searchMatches
|
|
2053
2156
|
}, i);
|
|
2054
2157
|
}), showThrobber && /* @__PURE__ */ jsx$1("box", {
|
|
2055
2158
|
style: { marginTop: items.length > 0 ? 1 : 0 },
|
|
@@ -2145,8 +2248,8 @@ function collapseCompletedSpawnStarts(events) {
|
|
|
2145
2248
|
}
|
|
2146
2249
|
return events.filter((_, i) => keep[i]);
|
|
2147
2250
|
}
|
|
2148
|
-
function partitionTranscript(events, settings) {
|
|
2149
|
-
const visible = collapseCompletedSpawnStarts(events).filter((
|
|
2251
|
+
function partitionTranscript(events, settings, searchReveal) {
|
|
2252
|
+
const visible = collapseCompletedSpawnStarts(events).filter((event) => isVisible(event, settings) || searchReveal !== void 0 && (event.depth ?? 0) === 0 && searchReveal(event));
|
|
2150
2253
|
if (visible.length === 0) return [];
|
|
2151
2254
|
if (settings.hideSubagentOutput) return visible.map((event, i) => ({
|
|
2152
2255
|
kind: "event",
|
|
@@ -2192,7 +2295,7 @@ function partitionTranscript(events, settings) {
|
|
|
2192
2295
|
* indented twice. Grandchildren (depth ≥ 2) still indent further, so
|
|
2193
2296
|
* nested subagents remain visually distinct.
|
|
2194
2297
|
*/
|
|
2195
|
-
function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds, codeSettleTargets, codeSettleEpoch = 0, live }) {
|
|
2298
|
+
function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds, codeSettleTargets, codeSettleEpoch = 0, live, searchQuery, searchTitleMatches }) {
|
|
2196
2299
|
const COLOR = useColors();
|
|
2197
2300
|
const childIds = useMemo(() => {
|
|
2198
2301
|
const set = /* @__PURE__ */ new Set();
|
|
@@ -2224,7 +2327,9 @@ function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds, cod
|
|
|
2224
2327
|
anchorId: anchorIds?.[i],
|
|
2225
2328
|
hideChildLabel,
|
|
2226
2329
|
settleCodeEpoch: codeSettleTargets?.has(evt) ? codeSettleEpoch : 0,
|
|
2227
|
-
liveSpawn: evt.kind === "spawn-start" ? live?.find((spawn) => spawn.id === evt.childId) : void 0
|
|
2330
|
+
liveSpawn: evt.kind === "spawn-start" ? live?.find((spawn) => spawn.id === evt.childId) : void 0,
|
|
2331
|
+
searchQuery,
|
|
2332
|
+
searchTitleMatch: evt.kind === "tool" && evt.callId !== void 0 && searchTitleMatches?.callIds.has(evt.callId) || (evt.kind === "spawn-start" || evt.kind === "spawn-end") && evt.childId !== void 0 && searchTitleMatches?.childIds.has(evt.childId)
|
|
2228
2333
|
}, i))
|
|
2229
2334
|
});
|
|
2230
2335
|
}
|
|
@@ -2233,8 +2338,9 @@ function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds, cod
|
|
|
2233
2338
|
* the left lane for the whole run; only the right tail changes:
|
|
2234
2339
|
* active `2 turns · 14s · ⠋ tool call`, done `4 turns`.
|
|
2235
2340
|
*/
|
|
2236
|
-
function SpawnLifecycleMarker({ row, dim, event, hideChildLabel, live }) {
|
|
2341
|
+
function SpawnLifecycleMarker({ row, dim, event, hideChildLabel, live, searchMatch = false }) {
|
|
2237
2342
|
const COLOR = useColors();
|
|
2343
|
+
const matchColor = resolveChipColor(useSurfaces().chips, "default");
|
|
2238
2344
|
const frame = useSpinnerFrame();
|
|
2239
2345
|
const terminal = event.kind === "spawn-end";
|
|
2240
2346
|
const failed = terminal && (event.text.startsWith("error") || event.text.startsWith("aborted"));
|
|
@@ -2259,7 +2365,8 @@ function SpawnLifecycleMarker({ row, dim, event, hideChildLabel, live }) {
|
|
|
2259
2365
|
children: terminal ? failed ? "✗ " : "🌳 " : "🌱 "
|
|
2260
2366
|
}),
|
|
2261
2367
|
!hideChildLabel && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
|
|
2262
|
-
fg: COLOR.brand,
|
|
2368
|
+
fg: searchMatch ? matchColor.fg : COLOR.brand,
|
|
2369
|
+
bg: searchMatch ? matchColor.bg : void 0,
|
|
2263
2370
|
children: event.childId ?? "child"
|
|
2264
2371
|
}), /* @__PURE__ */ jsx$1("span", {
|
|
2265
2372
|
fg: COLOR.mute,
|
|
@@ -2334,7 +2441,7 @@ function rowStyle(paddingLeft) {
|
|
|
2334
2441
|
alignSelf: "stretch"
|
|
2335
2442
|
};
|
|
2336
2443
|
}
|
|
2337
|
-
function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = false, selected = false, settleCodeEpoch = 0, liveSpawn }) {
|
|
2444
|
+
function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = false, selected = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false }) {
|
|
2338
2445
|
const COLOR = useColors();
|
|
2339
2446
|
const { settings } = useSettings();
|
|
2340
2447
|
const cleanText = stripTerminalControlChars(event.text);
|
|
@@ -2347,7 +2454,8 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
|
|
|
2347
2454
|
case "user-prompt": return /* @__PURE__ */ jsx$1(UserPromptBlock, {
|
|
2348
2455
|
text: safeText,
|
|
2349
2456
|
refs: event.refs,
|
|
2350
|
-
attachments: event.attachments
|
|
2457
|
+
attachments: event.attachments,
|
|
2458
|
+
searchQuery
|
|
2351
2459
|
});
|
|
2352
2460
|
case "info": return /* @__PURE__ */ jsx$1("box", {
|
|
2353
2461
|
style: row,
|
|
@@ -2405,7 +2513,8 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
|
|
|
2405
2513
|
children: [/* @__PURE__ */ jsx$1(ToolCallBlock, {
|
|
2406
2514
|
event,
|
|
2407
2515
|
display: settings.toolCallDisplay === "full" ? "full" : "formatted",
|
|
2408
|
-
dim: child
|
|
2516
|
+
dim: child,
|
|
2517
|
+
searchMatch: searchTitleMatch
|
|
2409
2518
|
}), event.tool === "todowrite" && /* @__PURE__ */ jsx$1(TodoInProgressList, {
|
|
2410
2519
|
input: event.input,
|
|
2411
2520
|
dim: child
|
|
@@ -2417,7 +2526,8 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
|
|
|
2417
2526
|
renderDefault: () => /* @__PURE__ */ jsx$1(ToolResultBlock, {
|
|
2418
2527
|
text: event.text,
|
|
2419
2528
|
indent: row.paddingLeft,
|
|
2420
|
-
maxLines: isUserBashEvent(event) ? settings.showFullShellOutput ? Number.MAX_SAFE_INTEGER : BASH_RESULT_MAX_LINES : void 0
|
|
2529
|
+
maxLines: isUserBashEvent(event) ? settings.showFullShellOutput ? Number.MAX_SAFE_INTEGER : BASH_RESULT_MAX_LINES : void 0,
|
|
2530
|
+
searchQuery
|
|
2421
2531
|
})
|
|
2422
2532
|
});
|
|
2423
2533
|
case "custom": return /* @__PURE__ */ jsx$1("box", {
|
|
@@ -2462,13 +2572,15 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
|
|
|
2462
2572
|
dim: COLOR.dim,
|
|
2463
2573
|
event,
|
|
2464
2574
|
hideChildLabel,
|
|
2465
|
-
live: liveSpawn
|
|
2575
|
+
live: liveSpawn,
|
|
2576
|
+
searchMatch: searchTitleMatch
|
|
2466
2577
|
});
|
|
2467
2578
|
case "spawn-end": return /* @__PURE__ */ jsx$1(SpawnLifecycleMarker, {
|
|
2468
2579
|
row,
|
|
2469
2580
|
dim: COLOR.dim,
|
|
2470
2581
|
event,
|
|
2471
|
-
hideChildLabel
|
|
2582
|
+
hideChildLabel,
|
|
2583
|
+
searchMatch: searchTitleMatch
|
|
2472
2584
|
});
|
|
2473
2585
|
case "compact-summary": return /* @__PURE__ */ jsx$1(CompactSummaryBlock, {
|
|
2474
2586
|
event,
|
|
@@ -2636,9 +2748,10 @@ function CompactSummaryBlock({ event, indent }) {
|
|
|
2636
2748
|
*/
|
|
2637
2749
|
/** Prompt chevron rendered ahead of every user-prompt block. */
|
|
2638
2750
|
const USER_PROMPT_PREFIX = "❯ ";
|
|
2639
|
-
function UserPromptBlock({ text, refs, attachments }) {
|
|
2751
|
+
function UserPromptBlock({ text, refs, attachments, searchQuery }) {
|
|
2640
2752
|
const COLOR = useColors();
|
|
2641
2753
|
const SURFACE = useSurfaces();
|
|
2754
|
+
const matchColor = resolveChipColor(SURFACE.chips, "default");
|
|
2642
2755
|
const boxStyle = {
|
|
2643
2756
|
border: true,
|
|
2644
2757
|
borderColor: COLOR.borderActive,
|
|
@@ -2671,20 +2784,30 @@ function UserPromptBlock({ text, refs, attachments }) {
|
|
|
2671
2784
|
})] }, `att-${idx}`);
|
|
2672
2785
|
})
|
|
2673
2786
|
}) : null;
|
|
2674
|
-
if (!refs || refs.length === 0)
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2787
|
+
if (!refs || refs.length === 0) {
|
|
2788
|
+
const ranges = searchQuery ? searchHighlightRanges(text, searchQuery) : [];
|
|
2789
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
2790
|
+
style: boxStyle,
|
|
2791
|
+
children: [
|
|
2792
|
+
text.length > 0 && /* @__PURE__ */ jsxs("text", {
|
|
2793
|
+
wrapMode: "word",
|
|
2794
|
+
children: [/* @__PURE__ */ jsx$1("span", {
|
|
2795
|
+
fg: COLOR.brand,
|
|
2796
|
+
children: USER_PROMPT_PREFIX
|
|
2797
|
+
}), highlightSegments(text, ranges).map((segment, i) => /* @__PURE__ */ jsx$1("span", {
|
|
2798
|
+
fg: segment.highlighted ? matchColor.fg : void 0,
|
|
2799
|
+
bg: segment.highlighted ? matchColor.bg : void 0,
|
|
2800
|
+
children: segment.text
|
|
2801
|
+
}, i))]
|
|
2802
|
+
}),
|
|
2803
|
+
!text.length && attachmentChips && /* @__PURE__ */ jsx$1("text", {
|
|
2804
|
+
fg: COLOR.brand,
|
|
2805
|
+
children: USER_PROMPT_PREFIX
|
|
2806
|
+
}),
|
|
2807
|
+
attachmentChips
|
|
2808
|
+
]
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2688
2811
|
const segments = splitPromptSegments(text, refs);
|
|
2689
2812
|
return /* @__PURE__ */ jsxs("box", {
|
|
2690
2813
|
style: boxStyle,
|
|
@@ -2697,7 +2820,14 @@ function UserPromptBlock({ text, refs, attachments }) {
|
|
|
2697
2820
|
fg: COLOR.brand,
|
|
2698
2821
|
children: USER_PROMPT_PREFIX
|
|
2699
2822
|
}), segments.map((seg, i) => {
|
|
2700
|
-
if (seg.kind === "plain")
|
|
2823
|
+
if (seg.kind === "plain") {
|
|
2824
|
+
const ranges = searchQuery ? searchHighlightRanges(seg.text, searchQuery) : [];
|
|
2825
|
+
return /* @__PURE__ */ jsx$1("text", { children: highlightSegments(seg.text, ranges).map((part, j) => /* @__PURE__ */ jsx$1("span", {
|
|
2826
|
+
fg: part.highlighted ? matchColor.fg : void 0,
|
|
2827
|
+
bg: part.highlighted ? matchColor.bg : void 0,
|
|
2828
|
+
children: part.text
|
|
2829
|
+
}, j)) }, i);
|
|
2830
|
+
}
|
|
2701
2831
|
const chip = resolveChipColor(SURFACE.chips, seg.providerId);
|
|
2702
2832
|
return /* @__PURE__ */ jsx$1("text", {
|
|
2703
2833
|
fg: chip.fg,
|
|
@@ -3132,33 +3262,64 @@ function CommandDetailBlock({ label, text, indent, diff }) {
|
|
|
3132
3262
|
})]
|
|
3133
3263
|
});
|
|
3134
3264
|
}
|
|
3135
|
-
function ToolResultBlock({ text, indent, diff, maxLines = TOOL_RESULT_MAX_LINES }) {
|
|
3265
|
+
function ToolResultBlock({ text, indent, diff, maxLines = TOOL_RESULT_MAX_LINES, searchQuery }) {
|
|
3136
3266
|
const COLOR = useColors();
|
|
3137
|
-
const
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3267
|
+
const matchColor = resolveChipColor(useSurfaces().chips, "default");
|
|
3268
|
+
const { lines, start } = useMemo(() => {
|
|
3269
|
+
const rawLines = stripAnsiSequences(text).split("\n");
|
|
3270
|
+
let end = rawLines.length;
|
|
3271
|
+
while (end > 0 && rawLines[end - 1].trim() === "") end--;
|
|
3272
|
+
const trimmed = rawLines.slice(0, end);
|
|
3273
|
+
let windowStart = 0;
|
|
3274
|
+
if (searchQuery && trimmed.length > maxLines) {
|
|
3275
|
+
const matchIdx = trimmed.findIndex((line) => searchHighlightRanges(line, searchQuery).length > 0);
|
|
3276
|
+
if (matchIdx >= maxLines) windowStart = Math.min(Math.max(0, matchIdx - Math.floor(maxLines / 2)), trimmed.length - maxLines);
|
|
3277
|
+
}
|
|
3278
|
+
return {
|
|
3279
|
+
lines: trimmed,
|
|
3280
|
+
start: windowStart
|
|
3281
|
+
};
|
|
3282
|
+
}, [
|
|
3283
|
+
text,
|
|
3284
|
+
searchQuery,
|
|
3285
|
+
maxLines
|
|
3286
|
+
]);
|
|
3287
|
+
if (lines.length === 0) return null;
|
|
3288
|
+
const visible = lines.slice(start, start + maxLines);
|
|
3289
|
+
const omittedBefore = start;
|
|
3290
|
+
const omitted = Math.max(0, lines.length - start - maxLines);
|
|
3144
3291
|
return /* @__PURE__ */ jsxs("box", {
|
|
3145
3292
|
style: {
|
|
3146
3293
|
paddingLeft: indent,
|
|
3147
3294
|
flexDirection: "column"
|
|
3148
3295
|
},
|
|
3149
|
-
children: [
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
fg: COLOR.
|
|
3159
|
-
children: "
|
|
3160
|
-
|
|
3161
|
-
|
|
3296
|
+
children: [
|
|
3297
|
+
omittedBefore > 0 && /* @__PURE__ */ jsxs("text", {
|
|
3298
|
+
fg: COLOR.mute,
|
|
3299
|
+
children: [/* @__PURE__ */ jsx$1("span", {
|
|
3300
|
+
fg: COLOR.borderActive,
|
|
3301
|
+
children: "┃ "
|
|
3302
|
+
}), `… ${omittedBefore} earlier line${omittedBefore === 1 ? "" : "s"}`]
|
|
3303
|
+
}),
|
|
3304
|
+
visible.map((line, i) => /* @__PURE__ */ jsxs("text", {
|
|
3305
|
+
fg: COLOR.mute,
|
|
3306
|
+
children: [/* @__PURE__ */ jsx$1("span", {
|
|
3307
|
+
fg: COLOR.borderActive,
|
|
3308
|
+
children: "┃ "
|
|
3309
|
+
}), diff && line ? /* @__PURE__ */ jsx$1(DiffStatLine, { line }) : highlightSegments(line || " ", searchQuery ? searchHighlightRanges(line, searchQuery) : []).map((segment, j) => /* @__PURE__ */ jsx$1("span", {
|
|
3310
|
+
fg: segment.highlighted ? matchColor.fg : COLOR.mute,
|
|
3311
|
+
bg: segment.highlighted ? matchColor.bg : void 0,
|
|
3312
|
+
children: segment.text
|
|
3313
|
+
}, j))]
|
|
3314
|
+
}, i)),
|
|
3315
|
+
omitted > 0 && /* @__PURE__ */ jsxs("text", {
|
|
3316
|
+
fg: COLOR.mute,
|
|
3317
|
+
children: [/* @__PURE__ */ jsx$1("span", {
|
|
3318
|
+
fg: COLOR.borderActive,
|
|
3319
|
+
children: "┃ "
|
|
3320
|
+
}), `… ${omitted} more line${omitted === 1 ? "" : "s"}`]
|
|
3321
|
+
})
|
|
3322
|
+
]
|
|
3162
3323
|
});
|
|
3163
3324
|
}
|
|
3164
3325
|
function EditDiffBlock({ payload, dim }) {
|
|
@@ -3515,8 +3676,9 @@ function HunkBlock({ index, hunk, outcome, tool, path, filetype, mdStyle, COLOR,
|
|
|
3515
3676
|
});
|
|
3516
3677
|
}
|
|
3517
3678
|
const FULL_VIEW_MAX_INPUT_BYTES = 32 * 1024;
|
|
3518
|
-
function ToolCallBlock({ event, display, dim }) {
|
|
3679
|
+
function ToolCallBlock({ event, display, dim, searchMatch = false }) {
|
|
3519
3680
|
const COLOR = useColors();
|
|
3681
|
+
const matchColor = resolveChipColor(useSurfaces().chips, "default");
|
|
3520
3682
|
const mdStyle = useMdStyle();
|
|
3521
3683
|
const name = event.tool ?? "";
|
|
3522
3684
|
const verb = displayNameFor(name, event.input);
|
|
@@ -3544,7 +3706,8 @@ function ToolCallBlock({ event, display, dim }) {
|
|
|
3544
3706
|
children: "↳ "
|
|
3545
3707
|
}),
|
|
3546
3708
|
/* @__PURE__ */ jsx$1("span", {
|
|
3547
|
-
fg: dim ? COLOR.dim : COLOR.model,
|
|
3709
|
+
fg: searchMatch ? matchColor.fg : dim ? COLOR.dim : COLOR.model,
|
|
3710
|
+
bg: searchMatch ? matchColor.bg : void 0,
|
|
3548
3711
|
children: verb
|
|
3549
3712
|
}),
|
|
3550
3713
|
/* @__PURE__ */ jsx$1("span", {
|
|
@@ -3576,7 +3739,8 @@ function ToolCallBlock({ event, display, dim }) {
|
|
|
3576
3739
|
children: "↳ "
|
|
3577
3740
|
}),
|
|
3578
3741
|
/* @__PURE__ */ jsx$1("span", {
|
|
3579
|
-
fg: dim ? COLOR.dim : COLOR.model,
|
|
3742
|
+
fg: searchMatch ? matchColor.fg : dim ? COLOR.dim : COLOR.model,
|
|
3743
|
+
bg: searchMatch ? matchColor.bg : void 0,
|
|
3580
3744
|
children: verb
|
|
3581
3745
|
}),
|
|
3582
3746
|
line?.target && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
|
|
@@ -4946,6 +5110,255 @@ async function resolveExtensionModalNode(node) {
|
|
|
4946
5110
|
return loaded;
|
|
4947
5111
|
}
|
|
4948
5112
|
//#endregion
|
|
5113
|
+
//#region src/tui/history-search-worker-client.ts
|
|
5114
|
+
let nextClientId = 1;
|
|
5115
|
+
/** Mirror of the TUI's ZIDANE_DEBUG-gated logger — silent by default. */
|
|
5116
|
+
const debugFallback = process.env.ZIDANE_DEBUG ? (op, error) => console.error(`[zidane/tui] history search ${op} fell back to the main thread:`, error) : () => {};
|
|
5117
|
+
function workerUrl() {
|
|
5118
|
+
if (typeof Bun !== "undefined" && Bun.isStandaloneExecutable) return new URL("./history-search-worker.ts", import.meta.url);
|
|
5119
|
+
return new URL(import.meta.url.endsWith(".ts") ? "./history-search-worker.ts" : "./history-search-worker.js", import.meta.url);
|
|
5120
|
+
}
|
|
5121
|
+
var HistorySearchWorkerClient = class {
|
|
5122
|
+
createWorker;
|
|
5123
|
+
worker = null;
|
|
5124
|
+
constructor(createWorker = () => new Worker(workerUrl(), {
|
|
5125
|
+
type: "module",
|
|
5126
|
+
smol: true
|
|
5127
|
+
})) {
|
|
5128
|
+
this.createWorker = createWorker;
|
|
5129
|
+
}
|
|
5130
|
+
indexes = /* @__PURE__ */ new WeakMap();
|
|
5131
|
+
registrations = /* @__PURE__ */ new Map();
|
|
5132
|
+
rebuilds = /* @__PURE__ */ new Map();
|
|
5133
|
+
pending = /* @__PURE__ */ new Map();
|
|
5134
|
+
nextRequestId = 1;
|
|
5135
|
+
unavailable = false;
|
|
5136
|
+
prewarm() {
|
|
5137
|
+
if (this.unavailable) return;
|
|
5138
|
+
try {
|
|
5139
|
+
this.ensureWorker();
|
|
5140
|
+
} catch {}
|
|
5141
|
+
}
|
|
5142
|
+
async build(turns, signal) {
|
|
5143
|
+
signal?.throwIfAborted();
|
|
5144
|
+
const indexId = `search-index-${nextClientId++}`;
|
|
5145
|
+
if (this.unavailable) return {
|
|
5146
|
+
indexId,
|
|
5147
|
+
length: turns.length,
|
|
5148
|
+
turns
|
|
5149
|
+
};
|
|
5150
|
+
try {
|
|
5151
|
+
return {
|
|
5152
|
+
...await this.request({
|
|
5153
|
+
type: "build",
|
|
5154
|
+
indexId,
|
|
5155
|
+
turns
|
|
5156
|
+
}, signal),
|
|
5157
|
+
turns
|
|
5158
|
+
};
|
|
5159
|
+
} catch (error) {
|
|
5160
|
+
if (signal?.aborted) throw signal.reason;
|
|
5161
|
+
debugFallback("build", error);
|
|
5162
|
+
return {
|
|
5163
|
+
indexId,
|
|
5164
|
+
length: turns.length,
|
|
5165
|
+
turns
|
|
5166
|
+
};
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
async search(index, query, limit, signal) {
|
|
5170
|
+
if (this.unavailable) return searchSessionIndexAsync(await this.fallbackIndex(index, signal), query, {
|
|
5171
|
+
limit,
|
|
5172
|
+
signal
|
|
5173
|
+
});
|
|
5174
|
+
try {
|
|
5175
|
+
const indexId = "indexId" in index ? index.indexId : await this.register(index);
|
|
5176
|
+
return await this.request({
|
|
5177
|
+
type: "search",
|
|
5178
|
+
indexId,
|
|
5179
|
+
query,
|
|
5180
|
+
limit
|
|
5181
|
+
}, signal);
|
|
5182
|
+
} catch (error) {
|
|
5183
|
+
if (signal?.aborted) throw signal.reason;
|
|
5184
|
+
if ("indexId" in index && error instanceof Error && error.message.startsWith("Unknown search index:")) {
|
|
5185
|
+
await this.rebuild(index);
|
|
5186
|
+
return this.request({
|
|
5187
|
+
type: "search",
|
|
5188
|
+
indexId: index.indexId,
|
|
5189
|
+
query,
|
|
5190
|
+
limit
|
|
5191
|
+
}, signal);
|
|
5192
|
+
}
|
|
5193
|
+
debugFallback("search", error);
|
|
5194
|
+
return searchSessionIndexAsync(await this.fallbackIndex(index, signal), query, {
|
|
5195
|
+
limit,
|
|
5196
|
+
signal
|
|
5197
|
+
});
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
async history(index, query, maxExcerpts, signal) {
|
|
5201
|
+
if (this.unavailable) return searchSessionHistoryIndexAsync(await this.fallbackIndex(index, signal), query, maxExcerpts, void 0, signal);
|
|
5202
|
+
try {
|
|
5203
|
+
const indexId = "indexId" in index ? index.indexId : await this.register(index);
|
|
5204
|
+
return await this.request({
|
|
5205
|
+
type: "history",
|
|
5206
|
+
indexId,
|
|
5207
|
+
query,
|
|
5208
|
+
maxExcerpts
|
|
5209
|
+
}, signal);
|
|
5210
|
+
} catch (error) {
|
|
5211
|
+
if (signal?.aborted) throw signal.reason;
|
|
5212
|
+
if ("indexId" in index && error instanceof Error && error.message.startsWith("Unknown search index:")) {
|
|
5213
|
+
await this.rebuild(index);
|
|
5214
|
+
return this.request({
|
|
5215
|
+
type: "history",
|
|
5216
|
+
indexId: index.indexId,
|
|
5217
|
+
query,
|
|
5218
|
+
maxExcerpts
|
|
5219
|
+
}, signal);
|
|
5220
|
+
}
|
|
5221
|
+
debugFallback("history", error);
|
|
5222
|
+
return searchSessionHistoryIndexAsync(await this.fallbackIndex(index, signal), query, maxExcerpts, void 0, signal);
|
|
5223
|
+
}
|
|
5224
|
+
}
|
|
5225
|
+
terminate() {
|
|
5226
|
+
this.worker?.terminate();
|
|
5227
|
+
this.worker = null;
|
|
5228
|
+
const error = /* @__PURE__ */ new Error("History search worker terminated");
|
|
5229
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
5230
|
+
this.pending.clear();
|
|
5231
|
+
this.registrations.clear();
|
|
5232
|
+
this.rebuilds.clear();
|
|
5233
|
+
}
|
|
5234
|
+
/** Re-register an evicted worker index; concurrent callers share one build. */
|
|
5235
|
+
rebuild(index) {
|
|
5236
|
+
const inFlight = this.rebuilds.get(index.indexId);
|
|
5237
|
+
if (inFlight) return inFlight;
|
|
5238
|
+
const rebuild = this.request({
|
|
5239
|
+
type: "build",
|
|
5240
|
+
indexId: index.indexId,
|
|
5241
|
+
turns: index.turns
|
|
5242
|
+
}).then(() => {}).finally(() => this.rebuilds.delete(index.indexId));
|
|
5243
|
+
this.rebuilds.set(index.indexId, rebuild);
|
|
5244
|
+
return rebuild;
|
|
5245
|
+
}
|
|
5246
|
+
fallbackIndex(index, signal) {
|
|
5247
|
+
if (!("indexId" in index)) return Promise.resolve(index);
|
|
5248
|
+
if (!index.fallback) index.fallback = buildSessionSearchIndexAsync(index.turns).catch((error) => {
|
|
5249
|
+
index.fallback = void 0;
|
|
5250
|
+
throw error;
|
|
5251
|
+
});
|
|
5252
|
+
signal?.throwIfAborted();
|
|
5253
|
+
return index.fallback;
|
|
5254
|
+
}
|
|
5255
|
+
ensureWorker() {
|
|
5256
|
+
if (this.worker) return this.worker;
|
|
5257
|
+
let worker;
|
|
5258
|
+
try {
|
|
5259
|
+
worker = this.createWorker();
|
|
5260
|
+
} catch (error) {
|
|
5261
|
+
this.disable(error);
|
|
5262
|
+
throw error;
|
|
5263
|
+
}
|
|
5264
|
+
worker.onmessage = (event) => this.onMessage(event.data);
|
|
5265
|
+
worker.onerror = (event) => this.disable(event.error ?? new Error(event.message));
|
|
5266
|
+
this.worker = worker;
|
|
5267
|
+
return worker;
|
|
5268
|
+
}
|
|
5269
|
+
register(index) {
|
|
5270
|
+
const known = this.indexes.get(index);
|
|
5271
|
+
if (known) {
|
|
5272
|
+
const pending = this.registrations.get(known);
|
|
5273
|
+
return pending ? pending.then(() => known) : Promise.resolve(known);
|
|
5274
|
+
}
|
|
5275
|
+
const indexId = `search-index-${nextClientId++}`;
|
|
5276
|
+
this.indexes.set(index, indexId);
|
|
5277
|
+
const registration = new Promise((resolve, reject) => {
|
|
5278
|
+
const worker = this.ensureWorker();
|
|
5279
|
+
let onRegistered;
|
|
5280
|
+
const onError = () => {
|
|
5281
|
+
worker.removeEventListener("message", onRegistered);
|
|
5282
|
+
this.registrations.delete(indexId);
|
|
5283
|
+
reject(/* @__PURE__ */ new Error("History search worker registration failed"));
|
|
5284
|
+
};
|
|
5285
|
+
onRegistered = (event) => {
|
|
5286
|
+
if (event.data.type !== "registered" || event.data.indexId !== indexId) return;
|
|
5287
|
+
worker.removeEventListener("message", onRegistered);
|
|
5288
|
+
worker.removeEventListener("error", onError);
|
|
5289
|
+
this.registrations.delete(indexId);
|
|
5290
|
+
resolve();
|
|
5291
|
+
};
|
|
5292
|
+
worker.addEventListener("message", onRegistered);
|
|
5293
|
+
worker.addEventListener("error", onError, { once: true });
|
|
5294
|
+
try {
|
|
5295
|
+
worker.postMessage({
|
|
5296
|
+
type: "register",
|
|
5297
|
+
indexId,
|
|
5298
|
+
index
|
|
5299
|
+
});
|
|
5300
|
+
} catch (error) {
|
|
5301
|
+
worker.removeEventListener("message", onRegistered);
|
|
5302
|
+
worker.removeEventListener("error", onError);
|
|
5303
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
5304
|
+
}
|
|
5305
|
+
});
|
|
5306
|
+
this.registrations.set(indexId, registration);
|
|
5307
|
+
return registration.then(() => indexId);
|
|
5308
|
+
}
|
|
5309
|
+
request(payload, signal) {
|
|
5310
|
+
signal?.throwIfAborted();
|
|
5311
|
+
const requestId = this.nextRequestId++;
|
|
5312
|
+
const worker = this.ensureWorker();
|
|
5313
|
+
return new Promise((resolve, reject) => {
|
|
5314
|
+
const abort = () => {
|
|
5315
|
+
worker.postMessage({
|
|
5316
|
+
type: "cancel",
|
|
5317
|
+
requestId
|
|
5318
|
+
});
|
|
5319
|
+
this.pending.delete(requestId);
|
|
5320
|
+
reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
|
|
5321
|
+
};
|
|
5322
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
5323
|
+
this.pending.set(requestId, {
|
|
5324
|
+
resolve: (value) => {
|
|
5325
|
+
signal?.removeEventListener("abort", abort);
|
|
5326
|
+
resolve(value);
|
|
5327
|
+
},
|
|
5328
|
+
reject: (error) => {
|
|
5329
|
+
signal?.removeEventListener("abort", abort);
|
|
5330
|
+
reject(error);
|
|
5331
|
+
}
|
|
5332
|
+
});
|
|
5333
|
+
try {
|
|
5334
|
+
worker.postMessage({
|
|
5335
|
+
...payload,
|
|
5336
|
+
requestId
|
|
5337
|
+
});
|
|
5338
|
+
} catch (error) {
|
|
5339
|
+
this.pending.delete(requestId);
|
|
5340
|
+
signal?.removeEventListener("abort", abort);
|
|
5341
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
5342
|
+
}
|
|
5343
|
+
});
|
|
5344
|
+
}
|
|
5345
|
+
onMessage(response) {
|
|
5346
|
+
if (response.requestId === void 0) return;
|
|
5347
|
+
const pending = this.pending.get(response.requestId);
|
|
5348
|
+
if (!pending) return;
|
|
5349
|
+
this.pending.delete(response.requestId);
|
|
5350
|
+
if (response.type === "error") pending.reject(new Error(response.message ?? "History search worker failed"));
|
|
5351
|
+
else pending.resolve(response.result);
|
|
5352
|
+
}
|
|
5353
|
+
disable(error) {
|
|
5354
|
+
this.unavailable = true;
|
|
5355
|
+
const reason = error instanceof Error ? error : new Error(String(error));
|
|
5356
|
+
for (const pending of this.pending.values()) pending.reject(reason);
|
|
5357
|
+
this.pending.clear();
|
|
5358
|
+
this.terminate();
|
|
5359
|
+
}
|
|
5360
|
+
};
|
|
5361
|
+
//#endregion
|
|
4949
5362
|
//#region src/tui/keybindings-modal.tsx
|
|
4950
5363
|
function KeybindingsModal({ bindings, extensionRows, filePath, onEditFile, onClose }) {
|
|
4951
5364
|
const SURFACE = useSurfaces();
|
|
@@ -5413,6 +5826,38 @@ function OptionRow({ option, isEnabled, isFocused, highlightBg }) {
|
|
|
5413
5826
|
*/
|
|
5414
5827
|
/** Visible rows in the windowed list. Keeps the modal a stable size on long catalogs. */
|
|
5415
5828
|
const VISIBLE_ROWS = 10;
|
|
5829
|
+
/**
|
|
5830
|
+
* Promote selected entries in SELECTION order, then append every unselected
|
|
5831
|
+
* row in its original catalog order. Pure/non-mutating and collision-safe:
|
|
5832
|
+
* duplicate/missing selection keys are ignored. Call only for an empty query
|
|
5833
|
+
* — search results must retain `filterModelCatalog`'s relevance order.
|
|
5834
|
+
*/
|
|
5835
|
+
function selectedModelsFirst(entries, selectedKeys, keyOf) {
|
|
5836
|
+
if (selectedKeys.length === 0) return [...entries];
|
|
5837
|
+
const byKey = new Map(entries.map((entry) => [keyOf(entry), entry]));
|
|
5838
|
+
const selected = [];
|
|
5839
|
+
const promoted = /* @__PURE__ */ new Set();
|
|
5840
|
+
for (const key of selectedKeys) {
|
|
5841
|
+
const entry = byKey.get(key);
|
|
5842
|
+
if (!entry || promoted.has(key)) continue;
|
|
5843
|
+
selected.push(entry);
|
|
5844
|
+
promoted.add(key);
|
|
5845
|
+
}
|
|
5846
|
+
return [...selected, ...entries.filter((entry) => !promoted.has(keyOf(entry)))];
|
|
5847
|
+
}
|
|
5848
|
+
function providerModelKey(providerKey, modelId) {
|
|
5849
|
+
return `${providerKey}\u0000${modelId}`;
|
|
5850
|
+
}
|
|
5851
|
+
/** Move one selected entry by one priority slot; boundary/missing = identity. */
|
|
5852
|
+
function reorderSelectedModel(selected, key, delta, keyOf) {
|
|
5853
|
+
const from = selected.findIndex((entry) => keyOf(entry) === key);
|
|
5854
|
+
const to = from + delta;
|
|
5855
|
+
if (from < 0 || to < 0 || to >= selected.length) return selected;
|
|
5856
|
+
const next = [...selected];
|
|
5857
|
+
const [entry] = next.splice(from, 1);
|
|
5858
|
+
next.splice(to, 0, entry);
|
|
5859
|
+
return next;
|
|
5860
|
+
}
|
|
5416
5861
|
function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onApplyChain }) {
|
|
5417
5862
|
const COLOR = useColors();
|
|
5418
5863
|
const SURFACE = useSurfaces();
|
|
@@ -5430,13 +5875,6 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
|
|
|
5430
5875
|
modelsFor,
|
|
5431
5876
|
current
|
|
5432
5877
|
]);
|
|
5433
|
-
const filtered = useMemo(() => filterModelCatalog(catalog, query), [catalog, query]);
|
|
5434
|
-
const [selectedIdx, setSelectedIdx] = useState(() => Math.max(0, indexOfEntry(catalog, current)));
|
|
5435
|
-
const handleQueryChange = useCallback((next) => {
|
|
5436
|
-
setQuery(next);
|
|
5437
|
-
setSelectedIdx(0);
|
|
5438
|
-
}, []);
|
|
5439
|
-
const safeIndex = filtered.length === 0 ? 0 : Math.min(selectedIdx, filtered.length - 1);
|
|
5440
5878
|
const chainMode = onApplyChain !== void 0;
|
|
5441
5879
|
const [initialChain] = useState(() => chain && chain.length > 0 ? [...chain] : [{
|
|
5442
5880
|
provider: current.providerKey,
|
|
@@ -5444,6 +5882,21 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
|
|
|
5444
5882
|
}]);
|
|
5445
5883
|
const [chainState, setChainState] = useState(initialChain);
|
|
5446
5884
|
const modalApi = useModalState();
|
|
5885
|
+
const filtered = useMemo(() => {
|
|
5886
|
+
const matches = filterModelCatalog(catalog, query);
|
|
5887
|
+
if (query.trim()) return matches;
|
|
5888
|
+
return selectedModelsFirst(matches, chainState.map((pick) => providerModelKey(pick.provider, pick.model)), (row) => providerModelKey(row.providerKey, row.model.id));
|
|
5889
|
+
}, [
|
|
5890
|
+
catalog,
|
|
5891
|
+
query,
|
|
5892
|
+
chainState
|
|
5893
|
+
]);
|
|
5894
|
+
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
5895
|
+
const handleQueryChange = useCallback((next) => {
|
|
5896
|
+
setQuery(next);
|
|
5897
|
+
setSelectedIdx(0);
|
|
5898
|
+
}, []);
|
|
5899
|
+
const safeIndex = filtered.length === 0 ? 0 : Math.min(selectedIdx, filtered.length - 1);
|
|
5447
5900
|
const commit = () => {
|
|
5448
5901
|
const row = filtered[safeIndex];
|
|
5449
5902
|
if (!row) return;
|
|
@@ -5479,6 +5932,26 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
|
|
|
5479
5932
|
};
|
|
5480
5933
|
}, [filtered, safeIndex]);
|
|
5481
5934
|
useKeyboard((key) => {
|
|
5935
|
+
if (key.ctrl && key.shift && (key.name === "up" || key.name === "down")) {
|
|
5936
|
+
if (chainMode) {
|
|
5937
|
+
const row = filtered[safeIndex];
|
|
5938
|
+
if (row) {
|
|
5939
|
+
const keyOf = (entry) => providerModelKey(entry.provider, entry.model);
|
|
5940
|
+
const selectedKey = providerModelKey(row.providerKey, row.model.id);
|
|
5941
|
+
const delta = key.name === "up" ? -1 : 1;
|
|
5942
|
+
const reordered = reorderSelectedModel(chainState, selectedKey, delta, keyOf);
|
|
5943
|
+
if (reordered !== chainState) {
|
|
5944
|
+
setChainState([...reordered]);
|
|
5945
|
+
if (!query.trim()) {
|
|
5946
|
+
const served = new Set(catalog.map((entry) => providerModelKey(entry.providerKey, entry.model.id)));
|
|
5947
|
+
const next = reordered.filter((entry) => served.has(keyOf(entry))).findIndex((entry) => keyOf(entry) === selectedKey);
|
|
5948
|
+
if (next >= 0) setSelectedIdx(next);
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
5953
|
+
return;
|
|
5954
|
+
}
|
|
5482
5955
|
if (key.name === "up") {
|
|
5483
5956
|
setSelectedIdx((i) => {
|
|
5484
5957
|
if (filtered.length === 0) return i;
|
|
@@ -5554,6 +6027,10 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
|
|
|
5554
6027
|
children: "↵"
|
|
5555
6028
|
}),
|
|
5556
6029
|
chainMode ? " add/remove · " : " select · ",
|
|
6030
|
+
chainMode && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
|
|
6031
|
+
fg: COLOR.warn,
|
|
6032
|
+
children: "ctrl+shift+↑↓"
|
|
6033
|
+
}), " reorder · "] }),
|
|
5557
6034
|
/* @__PURE__ */ jsx$1("span", {
|
|
5558
6035
|
fg: COLOR.warn,
|
|
5559
6036
|
children: "esc"
|
|
@@ -5606,23 +6083,41 @@ function ModelSelectList({ catalog, multi = false, initial, onApply, onCancel, v
|
|
|
5606
6083
|
return true;
|
|
5607
6084
|
});
|
|
5608
6085
|
}, [catalog]);
|
|
5609
|
-
const
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
const idx = first ? deduped.findIndex((e) => e.model.id === first) : -1;
|
|
5613
|
-
return Math.max(0, idx);
|
|
6086
|
+
const [initialPicks] = useState(() => {
|
|
6087
|
+
const known = new Set(deduped.map((e) => e.model.id));
|
|
6088
|
+
return initial.filter((id) => known.has(id));
|
|
5614
6089
|
});
|
|
6090
|
+
const [picks, setPicks] = useState(initialPicks);
|
|
6091
|
+
const filtered = useMemo(() => {
|
|
6092
|
+
const matches = filterModelCatalog(deduped, query);
|
|
6093
|
+
return query.trim() ? matches : selectedModelsFirst(matches, picks, (row) => row.model.id);
|
|
6094
|
+
}, [
|
|
6095
|
+
deduped,
|
|
6096
|
+
query,
|
|
6097
|
+
picks
|
|
6098
|
+
]);
|
|
6099
|
+
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
5615
6100
|
const handleQueryChange = useCallback((next) => {
|
|
5616
6101
|
setQuery(next);
|
|
5617
6102
|
setSelectedIdx(0);
|
|
5618
6103
|
}, []);
|
|
5619
6104
|
const safeIndex = filtered.length === 0 ? 0 : Math.min(selectedIdx, filtered.length - 1);
|
|
5620
|
-
const [initialPicks] = useState(() => {
|
|
5621
|
-
const known = new Set(deduped.map((e) => e.model.id));
|
|
5622
|
-
return initial.filter((id) => known.has(id));
|
|
5623
|
-
});
|
|
5624
|
-
const [picks, setPicks] = useState(initialPicks);
|
|
5625
6105
|
useKeyboard((key) => {
|
|
6106
|
+
if (key.ctrl && key.shift && (key.name === "up" || key.name === "down")) {
|
|
6107
|
+
if (multi) {
|
|
6108
|
+
const row = filtered[safeIndex];
|
|
6109
|
+
if (row) {
|
|
6110
|
+
const delta = key.name === "up" ? -1 : 1;
|
|
6111
|
+
const from = picks.indexOf(row.model.id);
|
|
6112
|
+
const reordered = reorderSelectedModel(picks, row.model.id, delta, (id) => id);
|
|
6113
|
+
if (reordered !== picks) {
|
|
6114
|
+
setPicks(reordered);
|
|
6115
|
+
if (!query.trim()) setSelectedIdx(from + delta);
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
return;
|
|
6120
|
+
}
|
|
5626
6121
|
if (key.name === "up" || key.name === "down") {
|
|
5627
6122
|
setSelectedIdx((i) => {
|
|
5628
6123
|
if (filtered.length === 0) return i;
|
|
@@ -5724,6 +6219,10 @@ function ModelSelectList({ catalog, multi = false, initial, onApply, onCancel, v
|
|
|
5724
6219
|
children: "↵"
|
|
5725
6220
|
}),
|
|
5726
6221
|
multi ? " add/remove · " : " select · ",
|
|
6222
|
+
multi && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
|
|
6223
|
+
fg: COLOR.warn,
|
|
6224
|
+
children: "ctrl+shift+↑↓"
|
|
6225
|
+
}), " reorder · "] }),
|
|
5727
6226
|
/* @__PURE__ */ jsx$1("span", {
|
|
5728
6227
|
fg: COLOR.warn,
|
|
5729
6228
|
children: "esc"
|
|
@@ -9294,21 +9793,27 @@ function isSessionRowId(rowId) {
|
|
|
9294
9793
|
}
|
|
9295
9794
|
/** Page-up / page-down jump size — half a typical visible window. */
|
|
9296
9795
|
const PAGE_JUMP = 6;
|
|
9297
|
-
function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreate, onFocusChange,
|
|
9796
|
+
function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreate, onFocusChange, contentMatches = /* @__PURE__ */ new Map(), contentSearching = false, contentSearchProgress = {
|
|
9797
|
+
done: 0,
|
|
9798
|
+
total: 0
|
|
9799
|
+
}, onSearchQueryChange, showAllProjects = false, onToggleScope, currentProjectRoot }) {
|
|
9298
9800
|
const focused = useModalAwareFocus();
|
|
9299
9801
|
const COLOR = useColors();
|
|
9300
9802
|
const inputRef = useRef(null);
|
|
9301
|
-
const
|
|
9302
|
-
const
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9803
|
+
const scrollboxRef = useRef(null);
|
|
9804
|
+
const [query, setQueryState] = useState("");
|
|
9805
|
+
const searchChangeRef = useRef(onSearchQueryChange);
|
|
9806
|
+
searchChangeRef.current = onSearchQueryChange;
|
|
9807
|
+
useEffect(() => () => searchChangeRef.current?.(""), []);
|
|
9808
|
+
const setQuery = useCallback((next) => {
|
|
9809
|
+
setQueryState(next);
|
|
9810
|
+
onSearchQueryChange?.(next);
|
|
9811
|
+
}, [onSearchQueryChange]);
|
|
9812
|
+
const filteredSessions = useMemo(() => filterSessionSearchCatalog(sessions, query, contentMatches), [
|
|
9813
|
+
sessions,
|
|
9814
|
+
query,
|
|
9815
|
+
contentMatches
|
|
9816
|
+
]);
|
|
9312
9817
|
const rows = useMemo(() => [{
|
|
9313
9818
|
kind: "new",
|
|
9314
9819
|
rowId: NEW_SESSION_ROW_ID,
|
|
@@ -9316,8 +9821,9 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
|
|
|
9316
9821
|
}, ...filteredSessions.map((meta) => ({
|
|
9317
9822
|
kind: "session",
|
|
9318
9823
|
rowId: meta.id,
|
|
9319
|
-
meta
|
|
9320
|
-
|
|
9824
|
+
meta,
|
|
9825
|
+
...query.trim() && contentMatches.get(meta.id) ? { contentMatch: contentMatches.get(meta.id) } : {}
|
|
9826
|
+
}))], [filteredSessions, contentMatches]);
|
|
9321
9827
|
useEffect(() => {
|
|
9322
9828
|
if (focused) inputRef.current?.focus();
|
|
9323
9829
|
}, [focused]);
|
|
@@ -9329,6 +9835,33 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
|
|
|
9329
9835
|
}
|
|
9330
9836
|
return rows.length > 1 ? 1 : 0;
|
|
9331
9837
|
}, [rows, focusedSessionId]);
|
|
9838
|
+
const lastScrolledCursorRef = useRef(-1);
|
|
9839
|
+
useEffect(() => {
|
|
9840
|
+
if (!focused) return;
|
|
9841
|
+
if (lastScrolledCursorRef.current === cursorIndex) return;
|
|
9842
|
+
const scrollbox = scrollboxRef.current;
|
|
9843
|
+
if (!scrollbox) return;
|
|
9844
|
+
let handle = 0;
|
|
9845
|
+
let attempts = 0;
|
|
9846
|
+
const align = () => {
|
|
9847
|
+
const child = scrollbox.content.findDescendantById(`session-row-${cursorIndex}`);
|
|
9848
|
+
if (child) {
|
|
9849
|
+
const childTop = child.y - scrollbox.content.y;
|
|
9850
|
+
const childBottom = childTop + child.height;
|
|
9851
|
+
if (childTop < scrollbox.scrollTop) scrollbox.scrollTop = childTop;
|
|
9852
|
+
else if (childBottom > scrollbox.scrollTop + scrollbox.viewport.height) scrollbox.scrollTop = childBottom - scrollbox.viewport.height;
|
|
9853
|
+
lastScrolledCursorRef.current = cursorIndex;
|
|
9854
|
+
return;
|
|
9855
|
+
}
|
|
9856
|
+
if (attempts++ < 5) handle = requestAnimationFrame(align);
|
|
9857
|
+
};
|
|
9858
|
+
handle = requestAnimationFrame(align);
|
|
9859
|
+
return () => cancelAnimationFrame(handle);
|
|
9860
|
+
}, [
|
|
9861
|
+
cursorIndex,
|
|
9862
|
+
focused,
|
|
9863
|
+
rows
|
|
9864
|
+
]);
|
|
9332
9865
|
useEffect(() => {
|
|
9333
9866
|
if (!onFocusChange) return;
|
|
9334
9867
|
if (focusedSessionId === "__new__") return;
|
|
@@ -9449,44 +9982,66 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
|
|
|
9449
9982
|
flexDirection: "column",
|
|
9450
9983
|
flexGrow: 1
|
|
9451
9984
|
},
|
|
9452
|
-
children: [
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9985
|
+
children: [
|
|
9986
|
+
/* @__PURE__ */ jsx$1("box", {
|
|
9987
|
+
style: {
|
|
9988
|
+
border: true,
|
|
9989
|
+
borderColor: COLOR.border,
|
|
9990
|
+
paddingLeft: 1,
|
|
9991
|
+
paddingRight: 1,
|
|
9992
|
+
height: 3,
|
|
9993
|
+
flexShrink: 0,
|
|
9994
|
+
marginBottom: 1
|
|
9995
|
+
},
|
|
9996
|
+
children: /* @__PURE__ */ jsx$1("input", {
|
|
9997
|
+
id: "session-search-input",
|
|
9998
|
+
ref: inputRef,
|
|
9999
|
+
focused,
|
|
10000
|
+
placeholder: "search sessions — title, project, or message content…",
|
|
10001
|
+
onInput: setQuery,
|
|
10002
|
+
onSubmit: () => {},
|
|
10003
|
+
style: { flexGrow: 1 }
|
|
10004
|
+
})
|
|
10005
|
+
}),
|
|
10006
|
+
/* @__PURE__ */ jsxs("scrollbox", {
|
|
10007
|
+
ref: scrollboxRef,
|
|
10008
|
+
id: "sessions-scrollbox",
|
|
10009
|
+
focusable: false,
|
|
10010
|
+
style: { flexGrow: 1 },
|
|
10011
|
+
children: [rows.map((row, idx) => /* @__PURE__ */ jsx$1(SessionRow, {
|
|
10012
|
+
anchorId: `session-row-${idx}`,
|
|
10013
|
+
row,
|
|
10014
|
+
separator: idx > 0,
|
|
10015
|
+
focused: idx === cursorIndex && focused,
|
|
10016
|
+
isCurrent: row.kind === "session" && row.rowId === currentId,
|
|
10017
|
+
showProject: showAllProjects,
|
|
10018
|
+
currentProjectRoot
|
|
10019
|
+
}, row.rowId)), filteredSessions.length === 0 && query.trim().length > 0 && /* @__PURE__ */ jsxs("text", {
|
|
10020
|
+
wrapMode: "none",
|
|
10021
|
+
children: [
|
|
10022
|
+
/* @__PURE__ */ jsx$1("span", {
|
|
10023
|
+
fg: COLOR.mute,
|
|
10024
|
+
children: contentSearching ? " searching conversation history for " : " no sessions match "
|
|
10025
|
+
}),
|
|
10026
|
+
/* @__PURE__ */ jsx$1("span", {
|
|
10027
|
+
fg: COLOR.warn,
|
|
10028
|
+
children: query.trim()
|
|
10029
|
+
}),
|
|
10030
|
+
contentSearching && /* @__PURE__ */ jsx$1("span", {
|
|
10031
|
+
fg: COLOR.mute,
|
|
10032
|
+
children: "…"
|
|
10033
|
+
})
|
|
10034
|
+
]
|
|
9487
10035
|
})]
|
|
9488
|
-
})
|
|
9489
|
-
|
|
10036
|
+
}),
|
|
10037
|
+
/* @__PURE__ */ jsx$1("box", {
|
|
10038
|
+
style: {
|
|
10039
|
+
height: 1,
|
|
10040
|
+
flexShrink: 0
|
|
10041
|
+
},
|
|
10042
|
+
children: contentSearching && /* @__PURE__ */ jsx$1(Spinner, { label: `searching conversation history ${contentSearchProgress.done}/${contentSearchProgress.total}` })
|
|
10043
|
+
})
|
|
10044
|
+
]
|
|
9490
10045
|
}), /* @__PURE__ */ jsx$1(TitleOverlay, {
|
|
9491
10046
|
title: "sessions",
|
|
9492
10047
|
meta: titleMeta
|
|
@@ -9502,18 +10057,25 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
|
|
|
9502
10057
|
* exactly the width of the focus + current markers above — so the first
|
|
9503
10058
|
* stat (the turn count) sits flush under the title's first letter.
|
|
9504
10059
|
*/
|
|
9505
|
-
function SessionRow({ row, focused, isCurrent, showProject = false, currentProjectRoot }) {
|
|
10060
|
+
function SessionRow({ row, anchorId, focused, isCurrent, separator = false, showProject = false, currentProjectRoot }) {
|
|
9506
10061
|
const COLOR = useColors();
|
|
10062
|
+
const SURFACE = useSurfaces();
|
|
9507
10063
|
const STATS_INDENT = " ";
|
|
9508
10064
|
const focusMark = focused ? "▶ " : " ";
|
|
9509
10065
|
const focusColor = focused ? COLOR.brand : COLOR.mute;
|
|
9510
10066
|
const titleColor = focused ? COLOR.brand : COLOR.dim;
|
|
10067
|
+
const rowStyle = {
|
|
10068
|
+
flexDirection: "column",
|
|
10069
|
+
flexShrink: 0,
|
|
10070
|
+
alignSelf: "stretch",
|
|
10071
|
+
...separator ? {
|
|
10072
|
+
border: ["top"],
|
|
10073
|
+
borderColor: COLOR.border
|
|
10074
|
+
} : {}
|
|
10075
|
+
};
|
|
9511
10076
|
if (row.kind === "new") return /* @__PURE__ */ jsxs("box", {
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
flexShrink: 0,
|
|
9515
|
-
alignSelf: "stretch"
|
|
9516
|
-
},
|
|
10077
|
+
id: anchorId,
|
|
10078
|
+
style: rowStyle,
|
|
9517
10079
|
children: [/* @__PURE__ */ jsxs("text", {
|
|
9518
10080
|
wrapMode: "none",
|
|
9519
10081
|
children: [
|
|
@@ -9545,11 +10107,8 @@ function SessionRow({ row, focused, isCurrent, showProject = false, currentProje
|
|
|
9545
10107
|
const currentMark = isCurrent ? "● " : " ";
|
|
9546
10108
|
const currentColor = isCurrent ? COLOR.accent : COLOR.mute;
|
|
9547
10109
|
return /* @__PURE__ */ jsxs("box", {
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
flexShrink: 0,
|
|
9551
|
-
alignSelf: "stretch"
|
|
9552
|
-
},
|
|
10110
|
+
id: anchorId,
|
|
10111
|
+
style: rowStyle,
|
|
9553
10112
|
children: [
|
|
9554
10113
|
/* @__PURE__ */ jsxs("text", {
|
|
9555
10114
|
wrapMode: "none",
|
|
@@ -9611,6 +10170,38 @@ function SessionRow({ row, focused, isCurrent, showProject = false, currentProje
|
|
|
9611
10170
|
fg: COLOR.mute,
|
|
9612
10171
|
children: STATS_INDENT
|
|
9613
10172
|
}), renderProjectLabel(meta.projectRoot, currentProjectRoot, COLOR)]
|
|
10173
|
+
}),
|
|
10174
|
+
row.contentMatch && /* @__PURE__ */ jsxs("box", {
|
|
10175
|
+
style: {
|
|
10176
|
+
flexDirection: "column",
|
|
10177
|
+
flexShrink: 0,
|
|
10178
|
+
marginTop: 1,
|
|
10179
|
+
marginLeft: 4,
|
|
10180
|
+
paddingLeft: 1,
|
|
10181
|
+
border: ["left"],
|
|
10182
|
+
borderColor: COLOR.border
|
|
10183
|
+
},
|
|
10184
|
+
children: [/* @__PURE__ */ jsxs("text", {
|
|
10185
|
+
wrapMode: "none",
|
|
10186
|
+
children: [/* @__PURE__ */ jsx$1("span", {
|
|
10187
|
+
fg: COLOR.brand,
|
|
10188
|
+
children: "matches"
|
|
10189
|
+
}), /* @__PURE__ */ jsx$1("span", {
|
|
10190
|
+
fg: COLOR.mute,
|
|
10191
|
+
children: ` · ${row.contentMatch.matchedTurnIds.length} message${row.contentMatch.matchedTurnIds.length === 1 ? "" : "s"}`
|
|
10192
|
+
})]
|
|
10193
|
+
}), row.contentMatch.excerpts.slice(0, 3).map((excerpt, i) => /* @__PURE__ */ jsxs("text", {
|
|
10194
|
+
fg: COLOR.dim,
|
|
10195
|
+
wrapMode: "none",
|
|
10196
|
+
children: [/* @__PURE__ */ jsx$1("span", {
|
|
10197
|
+
fg: COLOR.mute,
|
|
10198
|
+
children: `${excerpt.role === "user" ? "you" : excerpt.role} · `
|
|
10199
|
+
}), highlightSegments(excerpt.text, excerpt.highlighted).map((segment, j) => /* @__PURE__ */ jsx$1("span", {
|
|
10200
|
+
fg: segment.highlighted ? COLOR.warn : COLOR.dim,
|
|
10201
|
+
...segment.highlighted ? { bg: SURFACE.selection } : {},
|
|
10202
|
+
children: segment.text
|
|
10203
|
+
}, j))]
|
|
10204
|
+
}, `${excerpt.turnId}:${i}`))]
|
|
9614
10205
|
})
|
|
9615
10206
|
]
|
|
9616
10207
|
});
|
|
@@ -9708,7 +10299,10 @@ const MIME_BY_EXT = {
|
|
|
9708
10299
|
* default-value reference at the prop destructure.
|
|
9709
10300
|
*/
|
|
9710
10301
|
const EMPTY_QUEUED_MESSAGES = [];
|
|
9711
|
-
function
|
|
10302
|
+
function shouldRenderTranscriptSearch(searchMode, query) {
|
|
10303
|
+
return searchMode && query.trim().length > 0;
|
|
10304
|
+
}
|
|
10305
|
+
function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EMPTY_QUEUED_MESSAGES, queueSelectionIndex = null, queueShortcuts, onEnterQueueFromEmptyPrompt, settings, onSubmit, session, pending, onApproval, pendingInteraction, onInteraction, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, selectedTurnId, transcriptSearchMode = false, transcriptSearchQuery = "", transcriptSearchEvents = [], transcriptSearchSearching = false, onTranscriptSearchModeChange, onTranscriptSearchQueryChange, onTranscriptSearchNavigate, onTranscriptSearchOpen, onLoadEarlierTranscript, promptTriggerHints, attachImageBinding, editorBinding, sessionDetailsBinding, liveSession = null }) {
|
|
9712
10306
|
const COLOR = useColors();
|
|
9713
10307
|
const titleText = session?.title ?? "New session";
|
|
9714
10308
|
const sessionShortcutKey = formatBindingForDisplay(sessionDetailsBinding);
|
|
@@ -9766,6 +10360,8 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
|
|
|
9766
10360
|
hasStatusIcon
|
|
9767
10361
|
]);
|
|
9768
10362
|
const userPrompts = useMemo(() => promptHistoryFromEvents(events), [events]);
|
|
10363
|
+
const transcriptSearchActive = shouldRenderTranscriptSearch(transcriptSearchMode, transcriptSearchQuery);
|
|
10364
|
+
const displayedEvents = transcriptSearchActive ? transcriptSearchEvents : events;
|
|
9769
10365
|
const [completionPopupOpen, setCompletionPopupOpen] = useState(false);
|
|
9770
10366
|
const handlePopupOpenChange = useCallback((open) => {
|
|
9771
10367
|
setCompletionPopupOpen(open);
|
|
@@ -9793,10 +10389,13 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
|
|
|
9793
10389
|
visible: !fileEditPending
|
|
9794
10390
|
},
|
|
9795
10391
|
children: /* @__PURE__ */ jsx$1(Transcript, {
|
|
9796
|
-
events,
|
|
10392
|
+
events: displayedEvents,
|
|
9797
10393
|
settings,
|
|
9798
10394
|
selectedTurnId: selectedTurnId ?? null,
|
|
9799
|
-
|
|
10395
|
+
searchQuery: transcriptSearchActive ? transcriptSearchQuery : void 0,
|
|
10396
|
+
searchPending: transcriptSearchSearching,
|
|
10397
|
+
busy: !transcriptSearchActive && busy && !pending && !pendingInteraction,
|
|
10398
|
+
onLoadEarlier: transcriptSearchActive ? void 0 : onLoadEarlierTranscript
|
|
9800
10399
|
})
|
|
9801
10400
|
}),
|
|
9802
10401
|
/* @__PURE__ */ jsx$1(ExtensionStatusLine, {}),
|
|
@@ -9826,7 +10425,12 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
|
|
|
9826
10425
|
onCompletionTab,
|
|
9827
10426
|
enterSelectsCompletion,
|
|
9828
10427
|
onPopupOpenChange: handlePopupOpenChange,
|
|
9829
|
-
|
|
10428
|
+
searchMode: transcriptSearchMode,
|
|
10429
|
+
onSearchModeChange: onTranscriptSearchModeChange,
|
|
10430
|
+
onSearchQueryChange: onTranscriptSearchQueryChange,
|
|
10431
|
+
onSearchNavigate: onTranscriptSearchNavigate,
|
|
10432
|
+
onSearchOpen: onTranscriptSearchOpen,
|
|
10433
|
+
selectMode: queueSelectionIndex != null ? "queue" : null,
|
|
9830
10434
|
triggerHints: promptTriggerHints,
|
|
9831
10435
|
busy,
|
|
9832
10436
|
onEnterQueueFromEmpty: onEnterQueueFromEmptyPrompt,
|
|
@@ -10152,7 +10756,10 @@ const EMPTY_INLINE_PROVIDERS = [];
|
|
|
10152
10756
|
* ghost feels immediate on a pause.
|
|
10153
10757
|
*/
|
|
10154
10758
|
const INLINE_GHOST_DEBOUNCE_MS = 120;
|
|
10155
|
-
function
|
|
10759
|
+
function isTranscriptSearchExitKey(event, cursorOffset) {
|
|
10760
|
+
return event.name === "escape" || (event.name === "backspace" || event.name === "delete") && !event.ctrl && !event.meta && !event.shift && cursorOffset === 0;
|
|
10761
|
+
}
|
|
10762
|
+
function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, searchMode = false, onSearchModeChange, onSearchQueryChange, onSearchNavigate, onSearchOpen, selectMode = null, triggerHints, busy = false, onEnterQueueFromEmpty, attachImageBinding, editorBinding }) {
|
|
10156
10763
|
const focused = useModalAwareFocus();
|
|
10157
10764
|
const renderer = useRenderer();
|
|
10158
10765
|
const COLOR = useColors();
|
|
@@ -10217,7 +10824,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
10217
10824
|
* Once the user enters history (up at top), we snapshot the draft and cycle.
|
|
10218
10825
|
*/
|
|
10219
10826
|
const historyRef = useRef(null);
|
|
10220
|
-
const completion = useCompletion(bufferState, completionProviders ?? EMPTY_PROVIDERS);
|
|
10827
|
+
const completion = useCompletion(bufferState, searchMode ? EMPTY_PROVIDERS : completionProviders ?? EMPTY_PROVIDERS);
|
|
10221
10828
|
const popupOpen = completion.active != null && completion.items.length > 0;
|
|
10222
10829
|
useEffect(() => {
|
|
10223
10830
|
onPopupOpenChange?.(popupOpen);
|
|
@@ -10226,9 +10833,38 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
10226
10833
|
useChipHighlights(textareaRef, completion.references, chipStyle);
|
|
10227
10834
|
const [shellMode, setShellMode] = useState(false);
|
|
10228
10835
|
const shellModeRef = useRef(false);
|
|
10229
|
-
|
|
10230
|
-
|
|
10836
|
+
const searchModeRef = useRef(searchMode);
|
|
10837
|
+
const previousSearchModeRef = useRef(searchMode);
|
|
10838
|
+
const preSearchTextRef = useRef(null);
|
|
10839
|
+
/** Last text pushed into bufferState — distinguishes typed sentinels from bulk sets. */
|
|
10231
10840
|
const lastSyncedTextRef = useRef("");
|
|
10841
|
+
searchModeRef.current = searchMode;
|
|
10842
|
+
shellModeRef.current = shellMode;
|
|
10843
|
+
useEffect(() => {
|
|
10844
|
+
const textarea = textareaRef.current;
|
|
10845
|
+
if (!previousSearchModeRef.current && searchMode) {
|
|
10846
|
+
if (preSearchTextRef.current === null) {
|
|
10847
|
+
preSearchTextRef.current = textarea?.plainText ?? "";
|
|
10848
|
+
textarea?.clear();
|
|
10849
|
+
lastSyncedTextRef.current = "";
|
|
10850
|
+
setBufferState({
|
|
10851
|
+
text: "",
|
|
10852
|
+
cursor: 0
|
|
10853
|
+
});
|
|
10854
|
+
onSearchQueryChange?.("");
|
|
10855
|
+
}
|
|
10856
|
+
} else if (previousSearchModeRef.current && !searchMode) {
|
|
10857
|
+
const restored = preSearchTextRef.current ?? "";
|
|
10858
|
+
textarea?.setText(restored);
|
|
10859
|
+
lastSyncedTextRef.current = restored;
|
|
10860
|
+
setBufferState({
|
|
10861
|
+
text: restored,
|
|
10862
|
+
cursor: restored.length
|
|
10863
|
+
});
|
|
10864
|
+
preSearchTextRef.current = null;
|
|
10865
|
+
}
|
|
10866
|
+
previousSearchModeRef.current = searchMode;
|
|
10867
|
+
}, [searchMode, onSearchQueryChange]);
|
|
10232
10868
|
/**
|
|
10233
10869
|
* Flip shell mode with the ref mirror updated SYNCHRONOUSLY — callers
|
|
10234
10870
|
* that mutate the buffer and re-run `syncBuffer` in the same tick
|
|
@@ -10238,6 +10874,15 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
10238
10874
|
shellModeRef.current = on;
|
|
10239
10875
|
setShellMode(on);
|
|
10240
10876
|
}, []);
|
|
10877
|
+
const exitSearchMode = useCallback(() => {
|
|
10878
|
+
searchModeRef.current = false;
|
|
10879
|
+
onSearchQueryChange?.("");
|
|
10880
|
+
onSearchModeChange?.(false);
|
|
10881
|
+
}, [onSearchModeChange, onSearchQueryChange]);
|
|
10882
|
+
useKeyboard((key) => {
|
|
10883
|
+
if (!focused) return;
|
|
10884
|
+
if (searchModeRef.current && key.name === "escape") exitSearchMode();
|
|
10885
|
+
});
|
|
10241
10886
|
const [ghost, setGhost] = useState(null);
|
|
10242
10887
|
const ghostRef = useRef(null);
|
|
10243
10888
|
ghostRef.current = ghost;
|
|
@@ -10249,11 +10894,19 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
10249
10894
|
const syncBuffer = useCallback(() => {
|
|
10250
10895
|
const ta = textareaRef.current;
|
|
10251
10896
|
if (!ta) return;
|
|
10252
|
-
if (!shellModeRef.current && ta.plainText === "
|
|
10897
|
+
if (!searchModeRef.current && !shellModeRef.current && ta.plainText === "?" && lastSyncedTextRef.current === "") {
|
|
10898
|
+
ta.clear();
|
|
10899
|
+
preSearchTextRef.current = "";
|
|
10900
|
+
searchModeRef.current = true;
|
|
10901
|
+
onSearchModeChange?.(true);
|
|
10902
|
+
onSearchQueryChange?.("");
|
|
10903
|
+
}
|
|
10904
|
+
if (!searchModeRef.current && !shellModeRef.current && ta.plainText === "!" && lastSyncedTextRef.current === "") {
|
|
10253
10905
|
ta.clear();
|
|
10254
10906
|
setShellModeNow(true);
|
|
10255
10907
|
}
|
|
10256
10908
|
lastSyncedTextRef.current = ta.plainText;
|
|
10909
|
+
if (searchModeRef.current) onSearchQueryChange?.(ta.plainText);
|
|
10257
10910
|
setBufferState({
|
|
10258
10911
|
text: ta.plainText,
|
|
10259
10912
|
cursor: ta.cursorOffset
|
|
@@ -10266,7 +10919,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
10266
10919
|
const inlineEnabled = settings.inlineAutocomplete && inlineProviders.length > 0;
|
|
10267
10920
|
useEffect(() => {
|
|
10268
10921
|
const atWordBoundary = bufferState.cursor === bufferState.text.length || /\s/.test(bufferState.text[bufferState.cursor]);
|
|
10269
|
-
if (!inlineEnabled || popupOpen || selectMode !== null || shellMode || bufferState.text.length === 0 || !atWordBoundary) {
|
|
10922
|
+
if (!inlineEnabled || popupOpen || selectMode !== null || shellMode || searchMode || bufferState.text.length === 0 || !atWordBoundary) {
|
|
10270
10923
|
setGhost(null);
|
|
10271
10924
|
return;
|
|
10272
10925
|
}
|
|
@@ -10299,6 +10952,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
10299
10952
|
popupOpen,
|
|
10300
10953
|
selectMode,
|
|
10301
10954
|
shellMode,
|
|
10955
|
+
searchMode,
|
|
10302
10956
|
inlineProviders
|
|
10303
10957
|
]);
|
|
10304
10958
|
/**
|
|
@@ -10334,6 +10988,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
10334
10988
|
]);
|
|
10335
10989
|
const submit = useCallback(() => {
|
|
10336
10990
|
const value = textareaRef.current?.plainText ?? "";
|
|
10991
|
+
if (searchModeRef.current) return;
|
|
10337
10992
|
if (shellMode) {
|
|
10338
10993
|
if (!value.trim()) return;
|
|
10339
10994
|
onSubmit(`!${value}`, completion.references, attachments);
|
|
@@ -10501,6 +11156,24 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
10501
11156
|
openExternalEditor();
|
|
10502
11157
|
return;
|
|
10503
11158
|
}
|
|
11159
|
+
if (searchModeRef.current) {
|
|
11160
|
+
if (!event.ctrl && !event.meta && !event.shift && (event.name === "up" || event.name === "down")) {
|
|
11161
|
+
event.preventDefault();
|
|
11162
|
+
onSearchNavigate?.(event.name === "up" ? -1 : 1);
|
|
11163
|
+
return;
|
|
11164
|
+
}
|
|
11165
|
+
if (!event.ctrl && !event.meta && !event.shift && event.name === "return") {
|
|
11166
|
+
event.preventDefault();
|
|
11167
|
+
onSearchOpen?.();
|
|
11168
|
+
return;
|
|
11169
|
+
}
|
|
11170
|
+
if (isTranscriptSearchExitKey(event, textareaRef.current?.cursorOffset ?? 0)) {
|
|
11171
|
+
event.preventDefault();
|
|
11172
|
+
exitSearchMode();
|
|
11173
|
+
return;
|
|
11174
|
+
}
|
|
11175
|
+
return;
|
|
11176
|
+
}
|
|
10504
11177
|
if (event.name === "backspace" && !event.ctrl && !event.meta && !event.shift && shellModeRef.current && (textareaRef.current?.cursorOffset ?? 0) === 0) {
|
|
10505
11178
|
event.preventDefault();
|
|
10506
11179
|
setShellModeNow(false);
|
|
@@ -10611,7 +11284,10 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
10611
11284
|
editorBinding,
|
|
10612
11285
|
openExternalEditor,
|
|
10613
11286
|
attachments,
|
|
10614
|
-
showNotice
|
|
11287
|
+
showNotice,
|
|
11288
|
+
onSearchNavigate,
|
|
11289
|
+
onSearchOpen,
|
|
11290
|
+
exitSearchMode
|
|
10615
11291
|
]);
|
|
10616
11292
|
const ghostPosition = useMemo(() => {
|
|
10617
11293
|
if (!ghost) return null;
|
|
@@ -10692,7 +11368,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
10692
11368
|
children: [/* @__PURE__ */ jsxs("box", {
|
|
10693
11369
|
style: {
|
|
10694
11370
|
border: true,
|
|
10695
|
-
borderColor: selectMode ? COLOR.warn : shellMode ? COLOR.accent : COLOR.borderActive,
|
|
11371
|
+
borderColor: selectMode ? COLOR.warn : searchMode ? COLOR.warn : shellMode ? COLOR.accent : COLOR.borderActive,
|
|
10696
11372
|
paddingLeft: 1,
|
|
10697
11373
|
paddingRight: 1,
|
|
10698
11374
|
height: boxHeight,
|
|
@@ -10703,7 +11379,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
10703
11379
|
focused: focused && !selectMode,
|
|
10704
11380
|
keyBindings: TEXTAREA_BINDINGS,
|
|
10705
11381
|
wrapMode: "word",
|
|
10706
|
-
placeholder: selectMode === "
|
|
11382
|
+
placeholder: selectMode === "queue" ? "— queue-select mode — press ⎋ to resume typing —" : searchMode ? "Search this conversation…" : shellMode ? "Run a shell command…" : "Ask zidane…",
|
|
10707
11383
|
syntaxStyle: chipStyle,
|
|
10708
11384
|
style: {
|
|
10709
11385
|
flexGrow: 1,
|
|
@@ -10733,12 +11409,13 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
10733
11409
|
})]
|
|
10734
11410
|
}), /* @__PURE__ */ jsx$1(PromptHints, {
|
|
10735
11411
|
selectMode,
|
|
11412
|
+
searchMode,
|
|
10736
11413
|
triggerHints,
|
|
10737
11414
|
busy
|
|
10738
11415
|
})]
|
|
10739
11416
|
})
|
|
10740
11417
|
]
|
|
10741
|
-
}), !selectMode && /* @__PURE__ */ jsx$1("box", {
|
|
11418
|
+
}), !selectMode && !searchMode && /* @__PURE__ */ jsx$1("box", {
|
|
10742
11419
|
style: {
|
|
10743
11420
|
position: "absolute",
|
|
10744
11421
|
bottom: "100%",
|
|
@@ -10765,6 +11442,10 @@ const PROMPT_HINTS_NORMAL = [
|
|
|
10765
11442
|
key: "↑↓",
|
|
10766
11443
|
label: "history"
|
|
10767
11444
|
},
|
|
11445
|
+
{
|
|
11446
|
+
key: "?",
|
|
11447
|
+
label: "search"
|
|
11448
|
+
},
|
|
10768
11449
|
{
|
|
10769
11450
|
key: "ctrl+s",
|
|
10770
11451
|
label: "messages"
|
|
@@ -10793,8 +11474,8 @@ const PROMPT_HINTS_BUSY = [
|
|
|
10793
11474
|
label: "abort"
|
|
10794
11475
|
}
|
|
10795
11476
|
];
|
|
10796
|
-
/**
|
|
10797
|
-
const
|
|
11477
|
+
/** Conversation search is the message selector with an optional filter. */
|
|
11478
|
+
const PROMPT_HINTS_SEARCH = [
|
|
10798
11479
|
{
|
|
10799
11480
|
key: "↑↓",
|
|
10800
11481
|
label: "navigate"
|
|
@@ -10839,18 +11520,19 @@ const PROMPT_HINTS_SELECT_QUEUE = [{
|
|
|
10839
11520
|
* Swaps the hint set based on `selectMode` so the user sees only the
|
|
10840
11521
|
* shortcuts that actually do something in the active mode.
|
|
10841
11522
|
*/
|
|
10842
|
-
function PromptHints({ selectMode, triggerHints, busy = false }) {
|
|
11523
|
+
function PromptHints({ selectMode, searchMode = false, triggerHints, busy = false }) {
|
|
10843
11524
|
const COLOR = useColors();
|
|
10844
11525
|
const { settings } = useSettings();
|
|
10845
11526
|
const { width: termWidth } = useTerminalDimensions();
|
|
10846
|
-
const minimalResting = !selectMode && !busy && settings.uiMode === "minimal";
|
|
10847
|
-
const primary =
|
|
11527
|
+
const minimalResting = !selectMode && !searchMode && !busy && settings.uiMode === "minimal";
|
|
11528
|
+
const primary = searchMode ? PROMPT_HINTS_SEARCH : selectMode === "queue" ? PROMPT_HINTS_SELECT_QUEUE : busy ? PROMPT_HINTS_BUSY : minimalResting ? EMPTY_HINTS : PROMPT_HINTS_NORMAL;
|
|
10848
11529
|
const hints = useMemo(() => {
|
|
10849
11530
|
const budget = Math.max(0, termWidth - 5);
|
|
10850
11531
|
const tooLongCombined = !!triggerHints && triggerHints.length > 0 && !selectMode && primary.length > 0 && hintsLength(primary) + 3 + hintsLength(triggerHints) > budget;
|
|
10851
11532
|
return clipHintsToWidth(selectMode || !triggerHints || triggerHints.length === 0 || tooLongCombined ? primary : [...primary, ...triggerHints], budget);
|
|
10852
11533
|
}, [
|
|
10853
11534
|
selectMode,
|
|
11535
|
+
searchMode,
|
|
10854
11536
|
primary,
|
|
10855
11537
|
triggerHints,
|
|
10856
11538
|
termWidth
|
|
@@ -15496,7 +16178,8 @@ const PREVIEW_CHAR_MAX = 8e3;
|
|
|
15496
16178
|
* the Modal's own clamp; this is the cap on wide terminals so the modal
|
|
15497
16179
|
* keeps a comfortable shape rather than stretching to the full height.
|
|
15498
16180
|
*/
|
|
15499
|
-
const MAX_MODAL_HEIGHT =
|
|
16181
|
+
const MAX_MODAL_HEIGHT = 46;
|
|
16182
|
+
const MAX_MODAL_WIDTH = 148;
|
|
15500
16183
|
const EDIT_TEXTAREA_BINDINGS = (() => {
|
|
15501
16184
|
return [
|
|
15502
16185
|
...defaultTextareaKeyBindings.filter((b) => b.name !== "return" && !(b.name === "a" && b.ctrl && !b.shift && !b.meta)),
|
|
@@ -15524,12 +16207,18 @@ const EDIT_TEXTAREA_BINDINGS = (() => {
|
|
|
15524
16207
|
function editableText(turn) {
|
|
15525
16208
|
return turn.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
|
|
15526
16209
|
}
|
|
15527
|
-
function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
|
|
16210
|
+
function TurnDetailsModal({ turn, toolResultTurns = [], searchQuery, index, total, actions, keybindings }) {
|
|
15528
16211
|
const COLOR = useColors();
|
|
16212
|
+
const SURFACE = useSurfaces();
|
|
15529
16213
|
const modal = useModal();
|
|
15530
|
-
const fullText =
|
|
15531
|
-
|
|
15532
|
-
|
|
16214
|
+
const { fullText, previewEntries, summary } = useMemo(() => {
|
|
16215
|
+
const previewTurns = [turn, ...toolResultTurns];
|
|
16216
|
+
return {
|
|
16217
|
+
fullText: previewTurns.map(turnAsText).filter(Boolean).join("\n\n"),
|
|
16218
|
+
previewEntries: buildPreviewEntries(previewTurns, PREVIEW_CHAR_MAX),
|
|
16219
|
+
summary: blockSummary(previewTurns)
|
|
16220
|
+
};
|
|
16221
|
+
}, [turn, toolResultTurns]);
|
|
15533
16222
|
const bottomTitle = `${index - 1} before · ${total - index} after`;
|
|
15534
16223
|
const [pending, setPending] = useState(null);
|
|
15535
16224
|
const [copyStatus, setCopyStatus] = useState("idle");
|
|
@@ -15595,6 +16284,10 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
|
|
|
15595
16284
|
title: editing ? `edit turn ${index} / ${total} · ${turn.role}` : `turn ${index} / ${total} · ${turn.role}`,
|
|
15596
16285
|
bottomTitle: editing ? void 0 : bottomTitle,
|
|
15597
16286
|
maxHeight: MAX_MODAL_HEIGHT,
|
|
16287
|
+
maxWidth: MAX_MODAL_WIDTH,
|
|
16288
|
+
minWidth: 64,
|
|
16289
|
+
horizontalMargin: 2,
|
|
16290
|
+
verticalMargin: 1,
|
|
15598
16291
|
disableEscape: editing || pending !== null,
|
|
15599
16292
|
children: [
|
|
15600
16293
|
!editing && /* @__PURE__ */ jsxs("text", {
|
|
@@ -15674,7 +16367,8 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
|
|
|
15674
16367
|
title: " preview ",
|
|
15675
16368
|
style: {
|
|
15676
16369
|
border: true,
|
|
15677
|
-
borderColor: COLOR.
|
|
16370
|
+
borderColor: COLOR.borderActive,
|
|
16371
|
+
backgroundColor: SURFACE.background,
|
|
15678
16372
|
paddingLeft: 1,
|
|
15679
16373
|
paddingRight: 1,
|
|
15680
16374
|
flexDirection: "column",
|
|
@@ -15682,17 +16376,38 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
|
|
|
15682
16376
|
flexShrink: 1,
|
|
15683
16377
|
minHeight: 5
|
|
15684
16378
|
},
|
|
15685
|
-
children:
|
|
16379
|
+
children: previewEntries.length > 0 ? /* @__PURE__ */ jsx$1("scrollbox", {
|
|
15686
16380
|
focusable: false,
|
|
15687
|
-
style: {
|
|
16381
|
+
style: {
|
|
16382
|
+
flexGrow: 1,
|
|
16383
|
+
rootOptions: { backgroundColor: SURFACE.background },
|
|
16384
|
+
contentOptions: { flexDirection: "column" }
|
|
16385
|
+
},
|
|
15688
16386
|
stickyScroll: false,
|
|
15689
|
-
children: /* @__PURE__ */
|
|
15690
|
-
|
|
15691
|
-
|
|
15692
|
-
|
|
16387
|
+
children: previewEntries.map((entry, i) => /* @__PURE__ */ jsxs("box", {
|
|
16388
|
+
style: {
|
|
16389
|
+
flexDirection: "column",
|
|
16390
|
+
marginBottom: i === previewEntries.length - 1 ? 0 : 1
|
|
16391
|
+
},
|
|
16392
|
+
children: [/* @__PURE__ */ jsx$1("text", {
|
|
16393
|
+
fg: entry.error ? COLOR.error : entry.kind === "tool-result" ? COLOR.accent : entry.kind === "tool-call" ? COLOR.warn : COLOR.model,
|
|
16394
|
+
children: /* @__PURE__ */ jsx$1(HighlightedPreviewText, {
|
|
16395
|
+
text: entry.label,
|
|
16396
|
+
query: searchQuery,
|
|
16397
|
+
foreground: entry.error ? COLOR.error : entry.kind === "tool-result" ? COLOR.accent : entry.kind === "tool-call" ? COLOR.warn : COLOR.model
|
|
16398
|
+
})
|
|
16399
|
+
}), entry.text && /* @__PURE__ */ jsx$1("text", {
|
|
16400
|
+
fg: entry.error ? COLOR.error : COLOR.dim,
|
|
16401
|
+
children: /* @__PURE__ */ jsx$1(HighlightedPreviewText, {
|
|
16402
|
+
text: entry.text,
|
|
16403
|
+
query: searchQuery,
|
|
16404
|
+
foreground: entry.error ? COLOR.error : COLOR.dim
|
|
16405
|
+
})
|
|
16406
|
+
})]
|
|
16407
|
+
}, `${entry.kind}:${i}`))
|
|
15693
16408
|
}) : /* @__PURE__ */ jsx$1("text", {
|
|
15694
16409
|
fg: COLOR.mute,
|
|
15695
|
-
children: "— no
|
|
16410
|
+
children: "— no previewable content —"
|
|
15696
16411
|
})
|
|
15697
16412
|
}),
|
|
15698
16413
|
editing ? /* @__PURE__ */ jsxs("text", {
|
|
@@ -15852,14 +16567,97 @@ function ActionRow({ pending, copyStatus, canCopy, canEdit, forkKey, deleteKey,
|
|
|
15852
16567
|
]
|
|
15853
16568
|
});
|
|
15854
16569
|
}
|
|
15855
|
-
|
|
15856
|
-
|
|
15857
|
-
|
|
15858
|
-
|
|
15859
|
-
|
|
15860
|
-
|
|
16570
|
+
function buildPreviewEntries(turns, limit) {
|
|
16571
|
+
const entries = [];
|
|
16572
|
+
let remaining = limit;
|
|
16573
|
+
let skipped = 0;
|
|
16574
|
+
const add = (entry) => {
|
|
16575
|
+
if (remaining <= 0) {
|
|
16576
|
+
skipped++;
|
|
16577
|
+
return;
|
|
16578
|
+
}
|
|
16579
|
+
const text = entry.text.length > remaining ? `${entry.text.slice(0, remaining)}\n… (${entry.text.length - remaining} more chars)` : entry.text;
|
|
16580
|
+
entries.push({
|
|
16581
|
+
...entry,
|
|
16582
|
+
text
|
|
16583
|
+
});
|
|
16584
|
+
remaining -= Math.min(entry.text.length, remaining);
|
|
16585
|
+
};
|
|
16586
|
+
for (const turn of turns) for (const block of turn.content) if (block.type === "text") add({
|
|
16587
|
+
kind: "text",
|
|
16588
|
+
label: turn.role === "user" ? "you" : turn.role,
|
|
16589
|
+
text: block.text
|
|
16590
|
+
});
|
|
16591
|
+
else if (block.type === "thinking") add({
|
|
16592
|
+
kind: "thinking",
|
|
16593
|
+
label: "thinking",
|
|
16594
|
+
text: block.text
|
|
16595
|
+
});
|
|
16596
|
+
else if (block.type === "tool_call") add({
|
|
16597
|
+
kind: "tool-call",
|
|
16598
|
+
label: `tool call · ${block.name}`,
|
|
16599
|
+
text: stringifyPreview(block.input)
|
|
16600
|
+
});
|
|
16601
|
+
else if (block.type === "tool_result") add({
|
|
16602
|
+
kind: "tool-result",
|
|
16603
|
+
label: block.isError ? "tool result · error" : "tool result",
|
|
16604
|
+
text: toolResultPreview(block.output),
|
|
16605
|
+
error: block.isError
|
|
16606
|
+
});
|
|
16607
|
+
else if (block.type === "server_tool_use") add({
|
|
16608
|
+
kind: "tool-call",
|
|
16609
|
+
label: `server tool call · ${block.name}`,
|
|
16610
|
+
text: stringifyPreview(block.input)
|
|
16611
|
+
});
|
|
16612
|
+
else if (block.type === "server_tool_result") add({
|
|
16613
|
+
kind: "tool-result",
|
|
16614
|
+
label: `server tool result · ${block.toolName}`,
|
|
16615
|
+
text: stringifyPreview(block.content)
|
|
16616
|
+
});
|
|
16617
|
+
else if (block.type === "compact-summary") add({
|
|
16618
|
+
kind: "summary",
|
|
16619
|
+
label: `compaction summary · ${block.replacesTurnIds.length} turns`,
|
|
16620
|
+
text: block.summary
|
|
16621
|
+
});
|
|
16622
|
+
else if (block.type === "image" || block.type === "audio" || block.type === "video" || block.type === "document") add({
|
|
16623
|
+
kind: "attachment",
|
|
16624
|
+
label: `${block.type} attachment`,
|
|
16625
|
+
text: block.name ?? block.mediaType
|
|
16626
|
+
});
|
|
16627
|
+
else skipped++;
|
|
16628
|
+
if (skipped > 0) entries.push({
|
|
16629
|
+
kind: "summary",
|
|
16630
|
+
label: `… ${skipped} more block${skipped === 1 ? "" : "s"} not shown`,
|
|
16631
|
+
text: ""
|
|
16632
|
+
});
|
|
16633
|
+
return entries;
|
|
16634
|
+
}
|
|
16635
|
+
function HighlightedPreviewText({ text, query, foreground }) {
|
|
16636
|
+
const matchColor = resolveChipColor(useSurfaces().chips, "default");
|
|
16637
|
+
return /* @__PURE__ */ jsx$1(Fragment, { children: highlightSegments(text, query ? searchHighlightRanges(text, query) : []).map((segment, i) => /* @__PURE__ */ jsx$1("span", {
|
|
16638
|
+
fg: segment.highlighted ? matchColor.fg : foreground,
|
|
16639
|
+
bg: segment.highlighted ? matchColor.bg : void 0,
|
|
16640
|
+
children: segment.text
|
|
16641
|
+
}, i)) });
|
|
16642
|
+
}
|
|
16643
|
+
function toolResultPreview(output) {
|
|
16644
|
+
if (typeof output === "string") return output;
|
|
16645
|
+
return output.map((part) => {
|
|
16646
|
+
if (part.type === "text") return part.text;
|
|
16647
|
+
const name = "name" in part && part.name ? `: ${part.name}` : "";
|
|
16648
|
+
return `[${part.type} attachment${name}]`;
|
|
16649
|
+
}).join("\n");
|
|
16650
|
+
}
|
|
16651
|
+
function stringifyPreview(value) {
|
|
16652
|
+
try {
|
|
16653
|
+
return JSON.stringify(value, null, 2);
|
|
16654
|
+
} catch {
|
|
16655
|
+
return String(value);
|
|
16656
|
+
}
|
|
16657
|
+
}
|
|
16658
|
+
function blockSummary(turns) {
|
|
15861
16659
|
const counts = {};
|
|
15862
|
-
for (const block of turn.content) counts[block.type] = (counts[block.type] ?? 0) + 1;
|
|
16660
|
+
for (const turn of turns) for (const block of turn.content) counts[block.type] = (counts[block.type] ?? 0) + 1;
|
|
15863
16661
|
const parts = [];
|
|
15864
16662
|
for (const [type, n] of Object.entries(counts)) parts.push(`${n} ${type}`);
|
|
15865
16663
|
return parts.length === 0 ? "(empty)" : parts.join(" · ");
|
|
@@ -15915,6 +16713,8 @@ function lastLines(text, n) {
|
|
|
15915
16713
|
* and lstats — cheap enough that latency, not cost, picked the number.
|
|
15916
16714
|
*/
|
|
15917
16715
|
const EXTENSION_WATCH_POLL_MS = 3e3;
|
|
16716
|
+
/** LRU bound for cross-session search indexes (full lowercased text per session). */
|
|
16717
|
+
const SESSION_SEARCH_INDEX_CACHE_MAX = 64;
|
|
15918
16718
|
/**
|
|
15919
16719
|
* Surface failures that are normally silenced (teardown / save) when the
|
|
15920
16720
|
* `ZIDANE_DEBUG` env var is set. Logging via `console.error` would otherwise
|
|
@@ -16514,6 +17314,19 @@ function AppShell() {
|
|
|
16514
17314
|
const pickedRef = useRef(picked);
|
|
16515
17315
|
pickedRef.current = picked;
|
|
16516
17316
|
const [sessions, setSessions] = useState([]);
|
|
17317
|
+
const [sessionContentMatches, setSessionContentMatches] = useState(/* @__PURE__ */ new Map());
|
|
17318
|
+
const [sessionContentSearching, setSessionContentSearching] = useState(false);
|
|
17319
|
+
const [sessionContentSearchProgress, setSessionContentSearchProgress] = useState({
|
|
17320
|
+
done: 0,
|
|
17321
|
+
total: 0
|
|
17322
|
+
});
|
|
17323
|
+
const sessionSearchTimerRef = useRef(null);
|
|
17324
|
+
const sessionSearchQueryRef = useRef("");
|
|
17325
|
+
const sessionSearchGenerationRef = useRef(0);
|
|
17326
|
+
const sessionSearchAbortRef = useRef(null);
|
|
17327
|
+
const historySearchWorkerRef = useRef(null);
|
|
17328
|
+
const sessionSearchCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
17329
|
+
const liveSessionSearchIndexRef = useRef(null);
|
|
16517
17330
|
const [currentSession, setCurrentSession] = useState(null);
|
|
16518
17331
|
const [chatReferenceScope, setChatReferenceScope] = useState("project");
|
|
16519
17332
|
const [projectChatCatalog, setProjectChatCatalog] = useState([]);
|
|
@@ -16523,6 +17336,16 @@ function AppShell() {
|
|
|
16523
17336
|
const allChatCatalogRef = useRef(allChatCatalog);
|
|
16524
17337
|
allChatCatalogRef.current = allChatCatalog;
|
|
16525
17338
|
const [events, setEvents] = useState([]);
|
|
17339
|
+
const eventsRef = useRef(events);
|
|
17340
|
+
eventsRef.current = events;
|
|
17341
|
+
/**
|
|
17342
|
+
* True while the user is browsing revealed history (`onLoadEarlierTranscript`)
|
|
17343
|
+
* and no new run has started. Suspends the stream buffer's display cap so a
|
|
17344
|
+
* trailing smooth-mode drain (typewriter backlog outliving `runningRef`)
|
|
17345
|
+
* can't re-trim the page that was just loaded. Cleared wherever a run
|
|
17346
|
+
* starts and on session switch (`stream.reset()`), where the cap resumes.
|
|
17347
|
+
*/
|
|
17348
|
+
const transcriptRevealHoldRef = useRef(false);
|
|
16526
17349
|
const [busy, setBusy] = useState(false);
|
|
16527
17350
|
/**
|
|
16528
17351
|
* The provider+model the run actually advanced to after a `model:fallback`
|
|
@@ -16766,12 +17589,17 @@ function AppShell() {
|
|
|
16766
17589
|
*/
|
|
16767
17590
|
const lastInputTokensRef = useRef(0);
|
|
16768
17591
|
/**
|
|
16769
|
-
* Active turn
|
|
16770
|
-
*
|
|
16771
|
-
* has no highlight. When set, the prompt textarea is unfocused so up/down
|
|
16772
|
-
* navigate the turn list, ↵ opens the details modal, ⎋ exits the mode.
|
|
17592
|
+
* Active turn in conversation-search mode. The prompt remains focused for
|
|
17593
|
+
* filtering while plain arrows navigate and Enter opens turn details.
|
|
16773
17594
|
*/
|
|
16774
17595
|
const [selectedTurnId, setSelectedTurnId] = useState(null);
|
|
17596
|
+
const [transcriptSearchMode, setTranscriptSearchMode] = useState(false);
|
|
17597
|
+
const [transcriptSearchQuery, setTranscriptSearchQuery] = useState("");
|
|
17598
|
+
const [transcriptSearchResults, setTranscriptSearchResults] = useState([]);
|
|
17599
|
+
const [transcriptSearchSearching, setTranscriptSearchSearching] = useState(false);
|
|
17600
|
+
const transcriptSearchGenerationRef = useRef(0);
|
|
17601
|
+
const transcriptSearchAbortRef = useRef(null);
|
|
17602
|
+
const transcriptSearchIndexRef = useRef(null);
|
|
16775
17603
|
/**
|
|
16776
17604
|
* Highlighted entry in the type-ahead queue box. `null` means the user
|
|
16777
17605
|
* is in normal mode (textarea focused, queue purely informational);
|
|
@@ -17043,7 +17871,10 @@ function AppShell() {
|
|
|
17043
17871
|
* writes a title to a torn-down session.
|
|
17044
17872
|
*/
|
|
17045
17873
|
const autoTitleAbortRef = useRef(null);
|
|
17046
|
-
const stream = useStreamBuffer(setEvents, {
|
|
17874
|
+
const stream = useStreamBuffer(setEvents, {
|
|
17875
|
+
getSmooth: useCallback(() => smoothStreamingRef.current, []),
|
|
17876
|
+
maxTranscriptEvents: transcriptRevealHoldRef.current ? null : void 0
|
|
17877
|
+
});
|
|
17047
17878
|
streamRef.current = stream;
|
|
17048
17879
|
const makePicked = useCallback((provider, modelId) => {
|
|
17049
17880
|
const descriptor = providerRegistry[provider.key];
|
|
@@ -17802,6 +18633,113 @@ function AppShell() {
|
|
|
17802
18633
|
const toggleSessionsScope = useCallback(() => {
|
|
17803
18634
|
setSessionsScopeAll(!sessionsShowAll);
|
|
17804
18635
|
}, [sessionsShowAll]);
|
|
18636
|
+
/** Debounced, cached full-history search for the conversations list. */
|
|
18637
|
+
const onSessionSearchQueryChange = useCallback((raw) => {
|
|
18638
|
+
sessionSearchQueryRef.current = raw;
|
|
18639
|
+
const query = raw.trim();
|
|
18640
|
+
const generation = ++sessionSearchGenerationRef.current;
|
|
18641
|
+
sessionSearchAbortRef.current?.abort();
|
|
18642
|
+
const abortController = new AbortController();
|
|
18643
|
+
sessionSearchAbortRef.current = abortController;
|
|
18644
|
+
if (sessionSearchTimerRef.current) clearTimeout(sessionSearchTimerRef.current);
|
|
18645
|
+
if (!query) {
|
|
18646
|
+
setSessionContentMatches(/* @__PURE__ */ new Map());
|
|
18647
|
+
setSessionContentSearching(false);
|
|
18648
|
+
setSessionContentSearchProgress({
|
|
18649
|
+
done: 0,
|
|
18650
|
+
total: 0
|
|
18651
|
+
});
|
|
18652
|
+
return;
|
|
18653
|
+
}
|
|
18654
|
+
setSessionContentMatches(/* @__PURE__ */ new Map());
|
|
18655
|
+
setSessionContentSearching(true);
|
|
18656
|
+
setSessionContentSearchProgress({
|
|
18657
|
+
done: 0,
|
|
18658
|
+
total: sessions.length
|
|
18659
|
+
});
|
|
18660
|
+
sessionSearchTimerRef.current = setTimeout(() => {
|
|
18661
|
+
(async () => {
|
|
18662
|
+
const matches = /* @__PURE__ */ new Map();
|
|
18663
|
+
let cursor = 0;
|
|
18664
|
+
let done = 0;
|
|
18665
|
+
const worker = async () => {
|
|
18666
|
+
while (cursor < sessions.length && generation === sessionSearchGenerationRef.current) {
|
|
18667
|
+
const meta = sessions[cursor++];
|
|
18668
|
+
try {
|
|
18669
|
+
let index;
|
|
18670
|
+
const live = sessionRef.current;
|
|
18671
|
+
const cached = sessionSearchCacheRef.current.get(meta.id);
|
|
18672
|
+
if (live?.id === meta.id) {
|
|
18673
|
+
const cachedLive = liveSessionSearchIndexRef.current;
|
|
18674
|
+
if (cachedLive?.sessionId === meta.id && cachedLive.updatedAt === meta.updatedAt && cachedLive.turnCount === live.turns.length) index = cachedLive.index;
|
|
18675
|
+
else {
|
|
18676
|
+
index = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(live.turns, abortController.signal);
|
|
18677
|
+
liveSessionSearchIndexRef.current = {
|
|
18678
|
+
sessionId: meta.id,
|
|
18679
|
+
updatedAt: meta.updatedAt,
|
|
18680
|
+
turnCount: live.turns.length,
|
|
18681
|
+
index
|
|
18682
|
+
};
|
|
18683
|
+
}
|
|
18684
|
+
} else if (cached?.updatedAt === meta.updatedAt) {
|
|
18685
|
+
index = cached.index;
|
|
18686
|
+
sessionSearchCacheRef.current.delete(meta.id);
|
|
18687
|
+
sessionSearchCacheRef.current.set(meta.id, cached);
|
|
18688
|
+
} else {
|
|
18689
|
+
const turns = await store.getTurns(meta.id);
|
|
18690
|
+
index = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(turns, abortController.signal);
|
|
18691
|
+
const cache = sessionSearchCacheRef.current;
|
|
18692
|
+
cache.set(meta.id, {
|
|
18693
|
+
updatedAt: meta.updatedAt,
|
|
18694
|
+
index
|
|
18695
|
+
});
|
|
18696
|
+
while (cache.size > SESSION_SEARCH_INDEX_CACHE_MAX) {
|
|
18697
|
+
const oldest = cache.keys().next().value;
|
|
18698
|
+
if (oldest === void 0) break;
|
|
18699
|
+
cache.delete(oldest);
|
|
18700
|
+
}
|
|
18701
|
+
}
|
|
18702
|
+
if (generation !== sessionSearchGenerationRef.current) return;
|
|
18703
|
+
const match = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).history(index, query, 3, abortController.signal);
|
|
18704
|
+
if (match) matches.set(meta.id, match);
|
|
18705
|
+
} catch (error) {
|
|
18706
|
+
if (abortController.signal.aborted) throw abortController.signal.reason instanceof DOMException ? abortController.signal.reason : error;
|
|
18707
|
+
debugLog("session content search skipped a session", error);
|
|
18708
|
+
}
|
|
18709
|
+
done++;
|
|
18710
|
+
if (done === sessions.length || done % 4 === 0) setSessionContentSearchProgress({
|
|
18711
|
+
done,
|
|
18712
|
+
total: sessions.length
|
|
18713
|
+
});
|
|
18714
|
+
}
|
|
18715
|
+
};
|
|
18716
|
+
try {
|
|
18717
|
+
await Promise.all(Array.from({ length: Math.min(6, sessions.length) }, worker));
|
|
18718
|
+
} catch (error) {
|
|
18719
|
+
if (!(error instanceof DOMException && error.name === "AbortError")) throw error;
|
|
18720
|
+
return;
|
|
18721
|
+
}
|
|
18722
|
+
if (generation !== sessionSearchGenerationRef.current) return;
|
|
18723
|
+
setSessionContentMatches(matches);
|
|
18724
|
+
setSessionContentSearchProgress({
|
|
18725
|
+
done: sessions.length,
|
|
18726
|
+
total: sessions.length
|
|
18727
|
+
});
|
|
18728
|
+
setSessionContentSearching(false);
|
|
18729
|
+
})().catch((error) => {
|
|
18730
|
+
debugLog("session history search failed", error);
|
|
18731
|
+
if (generation === sessionSearchGenerationRef.current) setSessionContentSearching(false);
|
|
18732
|
+
});
|
|
18733
|
+
}, 120);
|
|
18734
|
+
}, [sessions, store]);
|
|
18735
|
+
useEffect(() => {
|
|
18736
|
+
if (sessionSearchQueryRef.current.trim()) onSessionSearchQueryChange(sessionSearchQueryRef.current);
|
|
18737
|
+
}, [sessions, onSessionSearchQueryChange]);
|
|
18738
|
+
useEffect(() => () => {
|
|
18739
|
+
if (sessionSearchTimerRef.current) clearTimeout(sessionSearchTimerRef.current);
|
|
18740
|
+
sessionSearchAbortRef.current?.abort();
|
|
18741
|
+
historySearchWorkerRef.current?.terminate();
|
|
18742
|
+
}, []);
|
|
17805
18743
|
const refreshSessions = useCallback(async () => {
|
|
17806
18744
|
const list = await listSessionMeta(store, sessionsShowAll ? void 0 : { projectRoot: projectDir });
|
|
17807
18745
|
setSessions(list);
|
|
@@ -17846,6 +18784,7 @@ function AppShell() {
|
|
|
17846
18784
|
debugLog("teardown: auto-title abort failed", err);
|
|
17847
18785
|
}
|
|
17848
18786
|
autoTitleAbortRef.current = null;
|
|
18787
|
+
transcriptRevealHoldRef.current = false;
|
|
17849
18788
|
stream.reset();
|
|
17850
18789
|
await agentRef.current?.destroy().catch((err) => debugLog("agent.destroy failed", err));
|
|
17851
18790
|
agentRef.current = null;
|
|
@@ -17909,6 +18848,7 @@ function AppShell() {
|
|
|
17909
18848
|
await session.appendTurns([turn]);
|
|
17910
18849
|
setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
|
|
17911
18850
|
runningRef.current = true;
|
|
18851
|
+
transcriptRevealHoldRef.current = false;
|
|
17912
18852
|
setBusy(true);
|
|
17913
18853
|
setActiveFallback(null);
|
|
17914
18854
|
try {
|
|
@@ -18310,6 +19250,25 @@ function AppShell() {
|
|
|
18310
19250
|
const onPopupOpenChange = useCallback((open) => {
|
|
18311
19251
|
popupOpenRef.current = open;
|
|
18312
19252
|
}, []);
|
|
19253
|
+
/**
|
|
19254
|
+
* Reveal one persisted-history page when Transcript reaches its top marker.
|
|
19255
|
+
* No full-history array is retained: materialize from Session on demand,
|
|
19256
|
+
* slice, then let the temporary array go. A running stream deliberately
|
|
19257
|
+
* declines — its first next append would re-cap anyway, and changing the
|
|
19258
|
+
* head while buckets are draining makes scroll preservation misleading.
|
|
19259
|
+
*/
|
|
19260
|
+
const onLoadEarlierTranscript = useCallback(() => {
|
|
19261
|
+
if (runningRef.current) return false;
|
|
19262
|
+
const session = sessionRef.current;
|
|
19263
|
+
const displayed = eventsRef.current;
|
|
19264
|
+
if (!session || hiddenEarlierTranscriptCount(displayed) <= 0) return false;
|
|
19265
|
+
const next = revealEarlierTranscriptEvents(eventsFromTurns(session.turns, session.runs), displayed);
|
|
19266
|
+
if (next === displayed) return false;
|
|
19267
|
+
transcriptRevealHoldRef.current = true;
|
|
19268
|
+
eventsRef.current = next;
|
|
19269
|
+
setEvents(next);
|
|
19270
|
+
return true;
|
|
19271
|
+
}, []);
|
|
18313
19272
|
const onAbort = useCallback(() => {
|
|
18314
19273
|
if (popupOpenRef.current) return;
|
|
18315
19274
|
commandAbortRef.current?.abort();
|
|
@@ -19249,6 +20208,7 @@ function AppShell() {
|
|
|
19249
20208
|
if (!agent || !session || !picked) return;
|
|
19250
20209
|
const wasRunning = runningRef.current;
|
|
19251
20210
|
if (!wasRunning) runningRef.current = true;
|
|
20211
|
+
transcriptRevealHoldRef.current = false;
|
|
19252
20212
|
suppressWakeRef.current = false;
|
|
19253
20213
|
(async () => {
|
|
19254
20214
|
let effectivePrompt = prompt;
|
|
@@ -19399,6 +20359,7 @@ function AppShell() {
|
|
|
19399
20359
|
if (suppressWakeRef.current) return;
|
|
19400
20360
|
if (!agent.hasPendingTaskNotifications) return;
|
|
19401
20361
|
runningRef.current = true;
|
|
20362
|
+
transcriptRevealHoldRef.current = false;
|
|
19402
20363
|
(async () => {
|
|
19403
20364
|
setBusy(true);
|
|
19404
20365
|
try {
|
|
@@ -19607,25 +20568,143 @@ function AppShell() {
|
|
|
19607
20568
|
}, [modal, config.paths.userDir]);
|
|
19608
20569
|
const hasMultipleAgents = useMemo(() => Object.keys(liveAgentRegistry).length > 1, [liveAgentRegistry]);
|
|
19609
20570
|
const turnIds = useMemo(() => selectableTurnIds(events, settings), [events, settings]);
|
|
20571
|
+
useEffect(() => {
|
|
20572
|
+
const query = transcriptSearchQuery.trim();
|
|
20573
|
+
const generation = ++transcriptSearchGenerationRef.current;
|
|
20574
|
+
transcriptSearchAbortRef.current?.abort();
|
|
20575
|
+
if (!transcriptSearchMode || !query) {
|
|
20576
|
+
setTranscriptSearchResults([]);
|
|
20577
|
+
setTranscriptSearchSearching(false);
|
|
20578
|
+
return;
|
|
20579
|
+
}
|
|
20580
|
+
const session = sessionRef.current;
|
|
20581
|
+
if (!session) {
|
|
20582
|
+
setTranscriptSearchResults([]);
|
|
20583
|
+
setTranscriptSearchSearching(false);
|
|
20584
|
+
return;
|
|
20585
|
+
}
|
|
20586
|
+
const turns = session.turns;
|
|
20587
|
+
const abortController = new AbortController();
|
|
20588
|
+
transcriptSearchAbortRef.current = abortController;
|
|
20589
|
+
setTranscriptSearchSearching(true);
|
|
20590
|
+
(async () => {
|
|
20591
|
+
const cached = transcriptSearchIndexRef.current;
|
|
20592
|
+
const index = cached?.sessionId === session.id && cached.events === events ? cached.index : await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(turns, abortController.signal);
|
|
20593
|
+
transcriptSearchIndexRef.current = {
|
|
20594
|
+
sessionId: session.id,
|
|
20595
|
+
events,
|
|
20596
|
+
index
|
|
20597
|
+
};
|
|
20598
|
+
const matches = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).search(index, query, turns.length, abortController.signal);
|
|
20599
|
+
if (generation !== transcriptSearchGenerationRef.current) return;
|
|
20600
|
+
const order = new Map(turns.map((turn, i) => [turn.id, i]));
|
|
20601
|
+
matches.sort((a, b) => (order.get(a.turn.id) ?? 0) - (order.get(b.turn.id) ?? 0));
|
|
20602
|
+
setTranscriptSearchResults(matches);
|
|
20603
|
+
setTranscriptSearchSearching(false);
|
|
20604
|
+
})().catch((error) => {
|
|
20605
|
+
if (!(error instanceof DOMException && error.name === "AbortError")) {
|
|
20606
|
+
debugLog("conversation search failed", error);
|
|
20607
|
+
if (generation === transcriptSearchGenerationRef.current) setTranscriptSearchSearching(false);
|
|
20608
|
+
}
|
|
20609
|
+
});
|
|
20610
|
+
return () => abortController.abort();
|
|
20611
|
+
}, [
|
|
20612
|
+
transcriptSearchMode,
|
|
20613
|
+
transcriptSearchQuery,
|
|
20614
|
+
events
|
|
20615
|
+
]);
|
|
20616
|
+
const transcriptSearchOwnerIds = useMemo(() => {
|
|
20617
|
+
const owners = /* @__PURE__ */ new Map();
|
|
20618
|
+
if (!transcriptSearchQuery.trim() || transcriptSearchResults.length === 0) return owners;
|
|
20619
|
+
const session = sessionRef.current;
|
|
20620
|
+
if (!session) return owners;
|
|
20621
|
+
const callOwner = /* @__PURE__ */ new Map();
|
|
20622
|
+
for (const turn of session.turns) for (const block of turn.content) if (block.type === "tool_call") callOwner.set(block.id, turn.id);
|
|
20623
|
+
for (const result of transcriptSearchResults) {
|
|
20624
|
+
const resultOwners = [];
|
|
20625
|
+
for (const block of result.turn.content) {
|
|
20626
|
+
if (block.type !== "tool_result") continue;
|
|
20627
|
+
const owner = callOwner.get(block.callId);
|
|
20628
|
+
if (owner && owner !== result.turn.id && !resultOwners.includes(owner)) resultOwners.push(owner);
|
|
20629
|
+
}
|
|
20630
|
+
if (resultOwners.length > 0) owners.set(result.turn.id, resultOwners);
|
|
20631
|
+
}
|
|
20632
|
+
return owners;
|
|
20633
|
+
}, [
|
|
20634
|
+
transcriptSearchQuery,
|
|
20635
|
+
transcriptSearchResults,
|
|
20636
|
+
events
|
|
20637
|
+
]);
|
|
20638
|
+
const transcriptSearchEvents = useMemo(() => {
|
|
20639
|
+
if (!transcriptSearchMode || !transcriptSearchQuery.trim()) return [];
|
|
20640
|
+
const session = sessionRef.current;
|
|
20641
|
+
if (!session) return [];
|
|
20642
|
+
const shown = transcriptSearchResults.slice(-200);
|
|
20643
|
+
const contextIds = new Set(shown.map((result) => result.turn.id));
|
|
20644
|
+
for (const result of shown) for (const owner of transcriptSearchOwnerIds.get(result.turn.id) ?? []) contextIds.add(owner);
|
|
20645
|
+
const searchEvents = eventsFromTurns(session.turns.filter((turn) => contextIds.has(turn.id)), session.runs);
|
|
20646
|
+
const hidden = transcriptSearchResults.length - shown.length;
|
|
20647
|
+
if (hidden > 0) searchEvents.unshift({
|
|
20648
|
+
kind: "info",
|
|
20649
|
+
text: `… ${hidden} earlier matched message${hidden === 1 ? "" : "s"} not shown — refine the query to narrow results.`
|
|
20650
|
+
});
|
|
20651
|
+
return searchEvents;
|
|
20652
|
+
}, [
|
|
20653
|
+
transcriptSearchMode,
|
|
20654
|
+
transcriptSearchQuery,
|
|
20655
|
+
transcriptSearchResults,
|
|
20656
|
+
transcriptSearchOwnerIds,
|
|
20657
|
+
events
|
|
20658
|
+
]);
|
|
20659
|
+
const transcriptSearchTurnIds = useMemo(() => {
|
|
20660
|
+
if (!transcriptSearchQuery.trim()) return turnIds;
|
|
20661
|
+
return selectableTurnIds(transcriptSearchEvents, settings, (event) => isVisibleDuringSearch(event, settings, transcriptSearchQuery));
|
|
20662
|
+
}, [
|
|
20663
|
+
transcriptSearchQuery,
|
|
20664
|
+
transcriptSearchEvents,
|
|
20665
|
+
turnIds,
|
|
20666
|
+
settings
|
|
20667
|
+
]);
|
|
19610
20668
|
/** Drop the selection if its turn disappeared (session swap, history reset). */
|
|
19611
20669
|
useEffect(() => {
|
|
20670
|
+
if (transcriptSearchMode) return;
|
|
19612
20671
|
if (selectedTurnId && !turnIds.includes(selectedTurnId)) setSelectedTurnId(null);
|
|
19613
|
-
}, [
|
|
19614
|
-
|
|
19615
|
-
|
|
19616
|
-
|
|
19617
|
-
|
|
20672
|
+
}, [
|
|
20673
|
+
selectedTurnId,
|
|
20674
|
+
turnIds,
|
|
20675
|
+
transcriptSearchMode
|
|
20676
|
+
]);
|
|
20677
|
+
useEffect(() => {
|
|
20678
|
+
if (!transcriptSearchMode) return;
|
|
20679
|
+
if (transcriptSearchTurnIds.length === 0) {
|
|
20680
|
+
setSelectedTurnId(null);
|
|
20681
|
+
return;
|
|
20682
|
+
}
|
|
20683
|
+
if (!selectedTurnId || !transcriptSearchTurnIds.includes(selectedTurnId)) setSelectedTurnId(transcriptSearchTurnIds[transcriptSearchTurnIds.length - 1] ?? null);
|
|
20684
|
+
}, [
|
|
20685
|
+
transcriptSearchMode,
|
|
20686
|
+
transcriptSearchTurnIds,
|
|
20687
|
+
selectedTurnId
|
|
20688
|
+
]);
|
|
20689
|
+
const onTranscriptSearchModeChange = useCallback((enabled) => {
|
|
20690
|
+
setTranscriptSearchMode(enabled);
|
|
20691
|
+
setTranscriptSearchQuery("");
|
|
20692
|
+
if (enabled) (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).prewarm();
|
|
20693
|
+
if (!enabled) setSelectedTurnId(null);
|
|
20694
|
+
else if (turnIds.length > 0) setSelectedTurnId(turnIds[turnIds.length - 1] ?? null);
|
|
19618
20695
|
}, [turnIds]);
|
|
19619
|
-
const cycleSelectedTurn = useCallback((direction) => {
|
|
20696
|
+
const cycleSelectedTurn = useCallback((direction, candidates = turnIds) => {
|
|
19620
20697
|
setSelectedTurnId((prev) => {
|
|
19621
|
-
if (!prev ||
|
|
19622
|
-
const idx =
|
|
19623
|
-
if (idx === -1) return
|
|
19624
|
-
const len =
|
|
19625
|
-
|
|
19626
|
-
return turnIds[nextIdx];
|
|
20698
|
+
if (!prev || candidates.length === 0) return prev;
|
|
20699
|
+
const idx = candidates.indexOf(prev);
|
|
20700
|
+
if (idx === -1) return candidates[candidates.length - 1];
|
|
20701
|
+
const len = candidates.length;
|
|
20702
|
+
return candidates[((idx + direction) % len + len) % len];
|
|
19627
20703
|
});
|
|
19628
20704
|
}, [turnIds]);
|
|
20705
|
+
const onTranscriptSearchNavigate = useCallback((direction) => {
|
|
20706
|
+
cycleSelectedTurn(direction, transcriptSearchTurnIds);
|
|
20707
|
+
}, [cycleSelectedTurn, transcriptSearchTurnIds]);
|
|
19629
20708
|
const onForkTurn = useCallback(async (turnId) => {
|
|
19630
20709
|
const source = sessionRef.current;
|
|
19631
20710
|
if (!source || !picked) return;
|
|
@@ -19665,7 +20744,10 @@ function AppShell() {
|
|
|
19665
20744
|
debugLog("delete: save failed", err);
|
|
19666
20745
|
return;
|
|
19667
20746
|
}
|
|
19668
|
-
|
|
20747
|
+
const nextEvents = capTranscriptEvents(eventsFromTurns(session.turns, session.runs));
|
|
20748
|
+
const nextIds = selectableTurnIds(nextEvents, settings);
|
|
20749
|
+
setEvents(nextEvents);
|
|
20750
|
+
setSelectedTurnId((prev) => prev ? selectionAfterTurnRemoval(turnIds, prev, nextIds) : prev);
|
|
19669
20751
|
const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
|
|
19670
20752
|
setLastInputTokens(replayedTokens);
|
|
19671
20753
|
lastInputTokensRef.current = replayedTokens;
|
|
@@ -19676,11 +20758,7 @@ function AppShell() {
|
|
|
19676
20758
|
userMessageCount: session.turns.reduce((n, t) => t.role === "user" ? n + 1 : n, 0),
|
|
19677
20759
|
updatedAt: Date.now()
|
|
19678
20760
|
} : prev);
|
|
19679
|
-
|
|
19680
|
-
if (!prev) return prev;
|
|
19681
|
-
return nextTurns.some((t) => t.id === prev) ? prev : null;
|
|
19682
|
-
});
|
|
19683
|
-
}, []);
|
|
20761
|
+
}, [settings, turnIds]);
|
|
19684
20762
|
const onEditTurn = useCallback(async (turnId, newText) => {
|
|
19685
20763
|
const session = sessionRef.current;
|
|
19686
20764
|
if (!session) return;
|
|
@@ -19702,7 +20780,10 @@ function AppShell() {
|
|
|
19702
20780
|
debugLog("edit: save failed", err);
|
|
19703
20781
|
return;
|
|
19704
20782
|
}
|
|
19705
|
-
|
|
20783
|
+
const rebuilt = eventsFromTurns(session.turns, session.runs);
|
|
20784
|
+
const patched = replaceTranscriptTurnEvents(eventsRef.current, rebuilt, turnId);
|
|
20785
|
+
setEvents(patched ?? capTranscriptEvents(rebuilt));
|
|
20786
|
+
setSelectedTurnId(turnId);
|
|
19706
20787
|
const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
|
|
19707
20788
|
setLastInputTokens(replayedTokens);
|
|
19708
20789
|
lastInputTokensRef.current = replayedTokens;
|
|
@@ -20000,11 +21081,23 @@ function AppShell() {
|
|
|
20000
21081
|
if (!session) return;
|
|
20001
21082
|
const turn = session.turns.find((t) => t.id === id);
|
|
20002
21083
|
if (!turn) return;
|
|
20003
|
-
const
|
|
21084
|
+
const candidates = transcriptSearchMode ? transcriptSearchTurnIds : turnIds;
|
|
21085
|
+
const index = candidates.indexOf(id) + 1;
|
|
21086
|
+
const callIds = new Set(turn.content.filter((block) => block.type === "tool_call").map((block) => block.id));
|
|
21087
|
+
const toolResultTurns = callIds.size === 0 ? [] : session.turns.flatMap((candidate) => {
|
|
21088
|
+
if (candidate.id === turn.id) return [];
|
|
21089
|
+
const content = candidate.content.filter((block) => block.type === "tool_result" && callIds.has(block.callId));
|
|
21090
|
+
return content.length > 0 ? [{
|
|
21091
|
+
...candidate,
|
|
21092
|
+
content
|
|
21093
|
+
}] : [];
|
|
21094
|
+
});
|
|
20004
21095
|
modal.open(/* @__PURE__ */ jsx$1(TurnDetailsModal, {
|
|
20005
21096
|
turn,
|
|
21097
|
+
toolResultTurns,
|
|
21098
|
+
searchQuery: transcriptSearchMode ? transcriptSearchQuery : void 0,
|
|
20006
21099
|
index,
|
|
20007
|
-
total:
|
|
21100
|
+
total: candidates.length,
|
|
20008
21101
|
actions: {
|
|
20009
21102
|
onFork: onForkTurn,
|
|
20010
21103
|
onDelete: onDeleteTurn,
|
|
@@ -20016,6 +21109,9 @@ function AppShell() {
|
|
|
20016
21109
|
modal,
|
|
20017
21110
|
selectedTurnId,
|
|
20018
21111
|
turnIds,
|
|
21112
|
+
transcriptSearchMode,
|
|
21113
|
+
transcriptSearchQuery,
|
|
21114
|
+
transcriptSearchTurnIds,
|
|
20019
21115
|
onForkTurn,
|
|
20020
21116
|
onDeleteTurn,
|
|
20021
21117
|
onEditTurn,
|
|
@@ -20023,23 +21119,8 @@ function AppShell() {
|
|
|
20023
21119
|
]);
|
|
20024
21120
|
useKeyboard((key) => {
|
|
20025
21121
|
if (modal.isOpen) return;
|
|
20026
|
-
if (
|
|
20027
|
-
if (key.name === "
|
|
20028
|
-
cycleSelectedTurn(-1);
|
|
20029
|
-
return;
|
|
20030
|
-
}
|
|
20031
|
-
if (key.name === "down") {
|
|
20032
|
-
cycleSelectedTurn(1);
|
|
20033
|
-
return;
|
|
20034
|
-
}
|
|
20035
|
-
if (key.name === "return") {
|
|
20036
|
-
openSelectedTurn();
|
|
20037
|
-
return;
|
|
20038
|
-
}
|
|
20039
|
-
if (key.name === "escape") {
|
|
20040
|
-
setSelectedTurnId(null);
|
|
20041
|
-
return;
|
|
20042
|
-
}
|
|
21122
|
+
if (transcriptSearchMode && screen === "chat") {
|
|
21123
|
+
if (key.name === "escape") onTranscriptSearchModeChange(false);
|
|
20043
21124
|
return;
|
|
20044
21125
|
}
|
|
20045
21126
|
if (queueSelectionIndex != null && screen === "chat") {
|
|
@@ -20204,8 +21285,8 @@ function AppShell() {
|
|
|
20204
21285
|
}));
|
|
20205
21286
|
return;
|
|
20206
21287
|
}
|
|
20207
|
-
if (matchesBinding(key, keybindings.enterSelectTurnMode) && screen === "chat" && !busy && !pendingApproval && !pendingInteraction) {
|
|
20208
|
-
|
|
21288
|
+
if (matchesBinding(key, keybindings.enterSelectTurnMode) && screen === "chat" && turnIds.length > 0 && !busy && !pendingApproval && !pendingInteraction) {
|
|
21289
|
+
onTranscriptSearchModeChange(true);
|
|
20209
21290
|
return;
|
|
20210
21291
|
}
|
|
20211
21292
|
if (matchesBinding(key, keybindings.cycleAgent) && screen === "chat" && hasMultipleAgents && !busy && !pendingApproval && !pendingInteraction) {
|
|
@@ -20496,6 +21577,10 @@ function AppShell() {
|
|
|
20496
21577
|
onPick: onSwitchSession,
|
|
20497
21578
|
onCreate: onCreateSession,
|
|
20498
21579
|
onFocusChange: setFocusedSessionId,
|
|
21580
|
+
contentMatches: sessionContentMatches,
|
|
21581
|
+
contentSearching: sessionContentSearching,
|
|
21582
|
+
contentSearchProgress: sessionContentSearchProgress,
|
|
21583
|
+
onSearchQueryChange: onSessionSearchQueryChange,
|
|
20499
21584
|
showAllProjects: sessionsShowAll,
|
|
20500
21585
|
onToggleScope: toggleSessionsScope,
|
|
20501
21586
|
currentProjectRoot: projectDir
|
|
@@ -20527,6 +21612,15 @@ function AppShell() {
|
|
|
20527
21612
|
enterSelectsCompletion: (providerId) => providerId === "chats",
|
|
20528
21613
|
onPopupOpenChange,
|
|
20529
21614
|
selectedTurnId,
|
|
21615
|
+
transcriptSearchMode,
|
|
21616
|
+
transcriptSearchQuery,
|
|
21617
|
+
transcriptSearchEvents,
|
|
21618
|
+
transcriptSearchSearching,
|
|
21619
|
+
onTranscriptSearchModeChange,
|
|
21620
|
+
onTranscriptSearchQueryChange: setTranscriptSearchQuery,
|
|
21621
|
+
onTranscriptSearchNavigate,
|
|
21622
|
+
onTranscriptSearchOpen: openSelectedTurn,
|
|
21623
|
+
onLoadEarlierTranscript,
|
|
20530
21624
|
promptTriggerHints,
|
|
20531
21625
|
attachImageBinding: keybindings.attachClipboardImage,
|
|
20532
21626
|
editorBinding: settings.externalEditor ? keybindings.openExternalEditor : void 0,
|
|
@@ -20538,7 +21632,7 @@ function AppShell() {
|
|
|
20538
21632
|
hints,
|
|
20539
21633
|
context: contextUsage,
|
|
20540
21634
|
cost: footerCost,
|
|
20541
|
-
status: pendingInteraction?.kind === "question" ? "asking" : compacting ? "compacting" : busy ? "busy" : null,
|
|
21635
|
+
status: transcriptSearchMode && transcriptSearchSearching ? "searching" : pendingInteraction?.kind === "question" ? "asking" : compacting ? "compacting" : busy ? "busy" : null,
|
|
20542
21636
|
compactionTokens,
|
|
20543
21637
|
compactionStartedAt
|
|
20544
21638
|
}),
|