zidane 6.6.4 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-BQqhozmt.js → acp-Wr8l4IDI.js} +7 -180
- package/dist/acp-Wr8l4IDI.js.map +1 -0
- package/dist/acp-cli.js +6 -6
- package/dist/acp.d.ts +1 -1
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +2 -1
- package/dist/{agent-DTxH7RHW.js → agent-C-camKQW.js} +162 -12
- package/dist/agent-C-camKQW.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/{anthropic-DRNRnd6W.js → anthropic-BDOGzhtj.js} +3 -3
- package/dist/{anthropic-DRNRnd6W.js.map → anthropic-BDOGzhtj.js.map} +1 -1
- package/dist/{transcript-anchors-BjH-68k2.js → approvals-CqqxE5Vh.js} +4924 -275
- package/dist/approvals-CqqxE5Vh.js.map +1 -0
- package/dist/{auth-7rv6ZfWk.js → auth-BdwDROYg.js} +12 -8
- package/dist/{auth-7rv6ZfWk.js.map → auth-BdwDROYg.js.map} +1 -1
- package/dist/chat/pure.d.ts +221 -4
- package/dist/chat/pure.d.ts.map +1 -0
- package/dist/chat/pure.js +4 -2
- package/dist/chat.d.ts +6 -2911
- package/dist/chat.js +8 -261
- package/dist/{context-transport-D8yeGdpd.js → context-transport-BqYwzJYE.js} +2 -2
- package/dist/{context-transport-D8yeGdpd.js.map → context-transport-BqYwzJYE.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +1 -1
- package/dist/contexts/daytona.js +1 -1
- package/dist/contexts/e2b.d.ts +1 -1
- package/dist/contexts/e2b.js +1 -1
- package/dist/daemon-cli.d.ts +1 -0
- package/dist/daemon-cli.js +90 -0
- package/dist/daemon-cli.js.map +1 -0
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +1 -1
- package/dist/extensions.d.ts +2 -2
- package/dist/extensions.js +2 -2
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +4 -4
- package/dist/{index-D0O-QO7Z.d.ts → index-B-FSaM3_.d.ts} +2863 -2375
- package/dist/index-B-FSaM3_.d.ts.map +1 -0
- package/dist/index-DgdzXRPH.d.ts +3590 -0
- package/dist/index-DgdzXRPH.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +14 -1179
- package/dist/ipc-CIv87xuH.js +331 -0
- package/dist/ipc-CIv87xuH.js.map +1 -0
- package/dist/ipc.d.ts +849 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +4 -0
- package/dist/json-rpc-rSWMUh9b.js +192 -0
- package/dist/json-rpc-rSWMUh9b.js.map +1 -0
- package/dist/{logger-DBX9uYOw.js → logger--C1VQjwJ.js} +3 -13
- package/dist/logger--C1VQjwJ.js.map +1 -0
- package/dist/logs-view-CWG8aUAG.js +44 -0
- package/dist/logs-view-CWG8aUAG.js.map +1 -0
- package/dist/{mcp-CVtSQTWg.js → mcp-BIWa57eY.js} +3 -3
- package/dist/{mcp-CVtSQTWg.js.map → mcp-BIWa57eY.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{messages-CZMUuKzo.js → messages-21XfHxyF.js} +3 -3
- package/dist/{messages-CZMUuKzo.js.map → messages-21XfHxyF.js.map} +1 -1
- package/dist/{openai-compat-BoTi-9fd.js → openai-compat-DY-AKxCg.js} +2 -2
- package/dist/{openai-compat-BoTi-9fd.js.map → openai-compat-DY-AKxCg.js.map} +1 -1
- package/dist/output/stream-json.d.ts +1 -1
- package/dist/output/terminal.d.ts +1 -1
- package/dist/{plugins-onDvTHEm.js → plugins-B531Tz5b.js} +2 -2
- package/dist/{plugins-onDvTHEm.js.map → plugins-B531Tz5b.js.map} +1 -1
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +1 -1
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +3 -3
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +1 -1
- package/dist/{providers-CmhdtD7w.js → providers-DX91jwir.js} +2 -2
- package/dist/{providers-CmhdtD7w.js.map → providers-DX91jwir.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +3 -3
- package/dist/restate.d.ts +1 -1
- package/dist/restate.js +8 -8
- package/dist/restate.js.map +1 -1
- package/dist/server-DJqp9dIR.js +1317 -0
- package/dist/server-DJqp9dIR.js.map +1 -0
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +42 -4
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-CyRLSsVa.js → session-K3icbPH9.js} +8 -2
- package/dist/session-K3icbPH9.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/{skills-BMGH_KK1.js → skills-PJyiSRCp.js} +3 -21
- package/dist/skills-PJyiSRCp.js.map +1 -0
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +1 -1
- package/dist/src-DzzhE2YO.js +2461 -0
- package/dist/src-DzzhE2YO.js.map +1 -0
- package/dist/todo-edits-Dw2oO2ON.js +378 -0
- package/dist/todo-edits-Dw2oO2ON.js.map +1 -0
- package/dist/{tool-formatters-NrzD6Fkk.d.ts → tool-formatters-DO56YBBw.d.ts} +104 -4
- package/dist/tool-formatters-DO56YBBw.d.ts.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +1 -1
- package/dist/{transcript-anchors-UhC58Tmj.d.ts → transcript-anchors-DXqWWGbq.d.ts} +53 -4
- package/dist/transcript-anchors-DXqWWGbq.d.ts.map +1 -0
- package/dist/tui.d.ts +94 -13
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +2241 -1009
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-mMxDRgLY.js → turn-operations-CZLsNgw-.js} +896 -11
- package/dist/turn-operations-CZLsNgw-.js.map +1 -0
- package/dist/{turn-operations-Cuc9pwiB.d.ts → turn-operations-DKFYWP3O.d.ts} +501 -4
- package/dist/turn-operations-DKFYWP3O.d.ts.map +1 -0
- package/dist/{types-BIarq1qE.js → types-CWgENVLY.js} +24 -2
- package/dist/types-CWgENVLY.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/xml-D5HBhJle.js +22 -0
- package/dist/xml-D5HBhJle.js.map +1 -0
- package/docs/CHAT.md +39 -0
- package/docs/IPC.md +420 -0
- package/docs/TUI.md +62 -0
- package/package.json +9 -3
- package/dist/acp-BQqhozmt.js.map +0 -1
- package/dist/agent-DTxH7RHW.js.map +0 -1
- package/dist/chat.d.ts.map +0 -1
- package/dist/chat.js.map +0 -1
- package/dist/index-D0O-QO7Z.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/inline-autocomplete-D0MPvBaS.js +0 -596
- package/dist/inline-autocomplete-D0MPvBaS.js.map +0 -1
- package/dist/logger-DBX9uYOw.js.map +0 -1
- package/dist/login-CB23LyI1.js +0 -707
- package/dist/login-CB23LyI1.js.map +0 -1
- package/dist/session-CyRLSsVa.js.map +0 -1
- package/dist/skills-BMGH_KK1.js.map +0 -1
- package/dist/tool-formatters-NrzD6Fkk.d.ts.map +0 -1
- package/dist/transcript-anchors-BjH-68k2.js.map +0 -1
- package/dist/transcript-anchors-UhC58Tmj.d.ts.map +0 -1
- package/dist/turn-operations-Cuc9pwiB.d.ts.map +0 -1
- package/dist/turn-operations-mMxDRgLY.js.map +0 -1
- package/dist/types-BIarq1qE.js.map +0 -1
package/dist/tui.js
CHANGED
|
@@ -1,34 +1,36 @@
|
|
|
1
|
-
import { C as estimateRebilledCostUsd } from "./openai-compat-
|
|
1
|
+
import { C as estimateRebilledCostUsd } from "./openai-compat-DY-AKxCg.js";
|
|
2
2
|
import { t as buildContextSegments } from "./context-segments-CJCtJ9sU.js";
|
|
3
|
-
import { $ as installExtensionFromSpec, $t as groupBindings, A as selectFilesFromSession, C as resolveLegacySessionDir, Cn as coerceExtensionConfigInput, D as resolveTasksDir, Dn as resolveExtensionConfigValue, E as resolveSessionDir, Et as isSpawnTool, G as summaryToTurn, It as looksBinary, M as compactConversation,
|
|
3
|
+
import { $ as installExtensionFromSpec, $t as groupBindings, A as selectFilesFromSession, C as resolveLegacySessionDir, Cn as coerceExtensionConfigInput, D as resolveTasksDir, Dn as resolveExtensionConfigValue, E as resolveSessionDir, Et as isSpawnTool, Fn as effectiveContextWindow, G as summaryToTurn, It as looksBinary, M as compactConversation, O as buildPostCompactAttachments, On as sanitizeExtensionConfigActionPatch, Pn as OUTPUT_RESERVE_TOKENS, Pt as tailTruncate, Qt as formatBindingForDisplay, Rt as createModelSearchTool, S as resolveLegacyPersistDir, T as resolvePersistDir, X as createExtensionPromptBridge, Xt as KEYBINDING_KEY_COL_WIDTH, Z as ExtensionInstallError, Zt as ensureKeybindingsFile, _n as parseExtensionScanPathEnv, at as createExtensionHud, bn as setDebugLogSink, bt as basic_default, dn as registerExtensionHostModules, dt as filterExtensionRegistry, en as keybindingsPath, f as applyCompactSummaryCutoff, ft as filterRegistryPlugins, hn as inlineLoadedExtension, ht as createExtensionHostRuntimeBridge, it as updateInstalledExtension, jn as createExtensionBus, lt as composeExtensionPreset, mt as partitionExtensionCatalog, on as resolveExtensionKeybindings, ot as discoverAndBuildExtensionRegistry, rt as removeInstalledExtension, t as createAgent, tn as matchesBinding, ut as disposeExtensionRegistry, v as cleanupPersistedSession, vn as scanExtensionDirectories, w as resolveMcpWarningsDir, x as rehomePersistedStubs, yn as createDebugLog } from "./agent-C-camKQW.js";
|
|
4
|
+
import { D as shouldAutoGenerateTitle, E as generateSessionTitle, F as parseCommandInvocation, M as commandBlocksToContent, P as executeCommand, S as resolveInlineSuggestion, a as loginMcpServer, b as createPathAutocompleteProvider, c as SKILL_EDIT_METADATA_KEY, g as readSkillEditMarker, h as extensionScaffold, j as commandBlockToEvent, l as ZIDANE_EXTENSION_SKILL_NAME, m as extensionDirForCwd, p as editSessionMetadata, u as ZIDANE_SKILL_EDIT_SKILL_NAME, v as withZidaneExtensionSkill, w as generateStructuredData, x as patchInlineGhost, y as withZidaneSkillEditSkill } from "./src-DzzhE2YO.js";
|
|
4
5
|
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";
|
|
5
6
|
import { n as appendCapped } from "./output-cap-C9zukcCj.js";
|
|
6
7
|
import { i as killProcessGroup, n as SUPPORTS_PROCESS_GROUPS, r as createProcessContext } from "./contexts-BSFQVclW.js";
|
|
7
8
|
import { u as errorMessage } from "./errors-DKiMrOKq.js";
|
|
8
|
-
import { E as buildCatalog, R as
|
|
9
|
-
import { n as
|
|
10
|
-
import { a as createRingBufferSink,
|
|
9
|
+
import { E as buildCatalog, R as findGitRoot, c as SkillInstallError, l as installSkillFromSpec } from "./skills-PJyiSRCp.js";
|
|
10
|
+
import { i as nextLogLevel, n as filterLogRecords, r as formatLogLine } from "./logs-view-CWG8aUAG.js";
|
|
11
|
+
import { a as createRingBufferSink, c as logFileTimestamp, i as createLoggingHooks, n as createFileSink, o as fanoutSink, r as createLogger } from "./logger--C1VQjwJ.js";
|
|
11
12
|
import { i as sniffImageMediaType } from "./media-sniff-UKijEaiQ.js";
|
|
12
|
-
import { _ as hasAuthorizationHeader, h as McpOAuthProvider, n as connectMcpServers, s as nonOwningMcpView } from "./mcp-
|
|
13
|
+
import { _ as hasAuthorizationHeader, h as McpOAuthProvider, n as connectMcpServers, s as nonOwningMcpView } from "./mcp-BIWa57eY.js";
|
|
13
14
|
import { replaceDynamicSection, splitSystemPrompt } from "./prompt.js";
|
|
14
|
-
import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-
|
|
15
|
-
import {
|
|
15
|
+
import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-K3icbPH9.js";
|
|
16
|
+
import { t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
|
|
16
17
|
import { s as withTimeout } from "./timeout-CjOG2mea.js";
|
|
17
18
|
import { i as installCacheBreakLogger } from "./cache-telemetry-ehVdWsg2.js";
|
|
18
|
-
import { a as generateStructuredData, c as shouldAutoGenerateTitle, f as commandBlockToEvent, g as parseCommandInvocation, h as executeCommand, n as patchInlineGhost, p as commandBlocksToContent, r as resolveInlineSuggestion, s as generateSessionTitle, t as createPathAutocompleteProvider } from "./inline-autocomplete-D0MPvBaS.js";
|
|
19
19
|
import { s as resolveCompletionSubmit } from "./completion-core-BRo86R_L.js";
|
|
20
|
-
import {
|
|
21
|
-
import { E as
|
|
20
|
+
import { n as JsonRpcRemoteError, t as JsonRpcConnectionClosedError } from "./json-rpc-rSWMUh9b.js";
|
|
21
|
+
import { $t as hasRunReceipt, A as chunkMarkdownForRender, At as SESSION_EVENT_STATUS_STYLE, B as DAEMON_HEALTH_STYLE, Bt as sessionEventExcerpt, C as usageTone, Cn as createChatsCompletionProvider, Ct as selectableTurnIds, E as filterModelCatalog, Et as turnSelectionOwnership, F as stripHtmlCommentsOutsideCode, Ht as sessionEventMeta, I as cacheBreakChip, K as filterChainFallbacks, Kt as fitRunReceiptCells, L as compactingLabel, M as codeTokenHasClosingFence, N as normalizeMarkdownCodeFences, O as parseCustomModelQuery, P as splitMarkdownCodeBlocks, Pt as isMainSession, Q as restoreChainFallbacks, S as resetLabel, St as retagUserPromptEventsFromRefs, T as buildModelCatalog, Tt as streamEventSearchText, U as chainPositionOf, Ut as shortSessionId, V as buildRunModel, W as chainsEqual, Wt as truncateTitle, X as providerKeySetChanged, Xt as formatRunReceiptLine, Yt as formatRunReceiptCells, Z as reorderKeyed, Zt as formatTokenCount, _ as turnContextSize, _n as resolveApprovalForPayload, _t as isVisible, a as TOOL_DISPLAY, b as formatUsagePercent, bn as summarizeOutcomes, cn as filetypeFromPath, d as finalizeStreamingMarkdown, dn as summarizeEditPayload, dt as buildLinearRamp, en as runReceiptFromRun, et as toggleChainEntry, f as finalizeStreamingMarkdownForOwner, ft as BASH_CALL_ID_PREFIX, g as revealEarlierTranscriptEvents, gn as parseEditOutcomesFromResult, gt as isUserBashEvent, h as replaceTranscriptTurnEvents, hn as mergeApprovalAndBodyOutcomes, ht as isTurnHighlighted, i as turnAsText, in as clipDiffForRender, it as uniqueSkillNamesFromReferences, j as closeDanglingCodeFence, ln as previewEditPayload, lt as createCommandsCompletionProvider, mt as isEditErrorResult, n as deleteTurnSafely, nn as buildContextualDiff, o as displayNameFor, ot as createFilesCompletionProvider, p as hiddenEarlierTranscriptCount, pn as buildEditOutcomesAnnotation, pt as EDIT_TOOL_NAMES, q as pickChainPrimary, qt as formatCostUsd, r as truncateTurnsAt, rn as buildUnifiedDiff, rt as createSkillsCompletionProvider, s as formatToolCall, sn as extractEditPayload, u as capTranscriptEvents, ut as blendHsl, v as formatBalance, vn as rewriteMultiEditHeader, vt as isVisibleDuringSearch, w as splitPromptSegments, wt as selectionAfterTurnRemoval, x as mostConstrainedWindow, xt as retagUserPromptEvents, yn as stripEditOutcomesAnnotation, yt as promptHistoryFromEvents, zt as sessionDirLabel } from "./turn-operations-CZLsNgw-.js";
|
|
22
|
+
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
|
+
import { $ as supportsOAuth, $n as buildUpdateHint, $r as chatReferenceTokensForPrompt, A as useSafeModeQueue, An as createDiscoverySlot, At as makeRequestInteraction, Bn as hasClaudeCodeSetup, Ct as InteractionsProvider, Dt as createInteractionTools, Er as TODO_STATUS_GLYPHS, Et as createAutomaticInteractionResponse, Fn as useConfig, G as usageChecksOptedOut, Gn as buildMcpServers, H as fetchAllProviderUsage, Hn as mcpsWritePath, In as resolveConfig, Ir as selectActiveTodos, J as fetchOAuthRedirect, Jn as discoverProjectMcps, Jt as buildPluginContributions, Kn as dedupMcpServersByName, Ln as resolveStoragePaths, Lr as setTodosForRun, M as addToSafelist, Mn as useDiscovery, N as getSafelist, Nn as useDiscoveryOptional, Nr as pickActiveRunId, Nt as useInteractionsActions, O as SafeModeProvider, On as parseEditorCommand, Or as applyTodosSnapshot, P as isOnSafelist, Pn as ConfigProvider, Pr as pruneTodosByRun, Pt as useInteractionsQueue, Q as runOAuthLogin, Qn as bootTick, Qr as envSection, R as suggestSafelistEntries, Rn as useCompletion, Rr as todoProgressLabel, St as HOST_INTERACTION_ID_PREFIX, Tt as buildResumedToolResultsTurn, U as fetchProviderUsage, Un as removeMcpServer, Ut as createBuiltinCommands, V as clearProviderUsageCache, Vn as importFromClaudeCode, W as providerUsageSupported, Wn as upsertMcpServer, Wt as createMainSession, X as redirectUriFromAuthUrl, Xn as tryOpenBrowser, Xr as buildBuildSystem, Xt as parsePluginScanPathEnv, Yt as discoverProjectPlugins, Z as oauthUsesManualCodePaste, Zr as buildPlanSystem, Zt as attachMcpWarnLog, _ as buildSpawnEndEvent, _i as toolCallPreview, _t as useMcpAuthState, ai as eventsFromTurns, at as subscribeMcpToolsCache, b as defaultSkillScanPaths, bi as usageByModel, bn as resolveTheme, bt as ASK_USER_TOOL, c as useSelectStyle, ci as listSessionMeta, cn as DEFAULT_SETTINGS, d as useTheme, di as noticeTurnContent, dn as SETTINGS_INPUTS, dr as discoverAgentsMd, dt as parentServerName, ei as createChatTurnsReadTool, en as EMPTY_HINTS, er as useUpdateCheck, f as discoverSystemMd, fn as SETTINGS_TOGGLES, ft as createFileMcpCredentialStore, g as useStreamBuffer, gn as useSettings, gt as useMcpAuthDispatch, h as userSystemMdPath, hi as sumRunCosts, ht as McpAuthProvider, ii as deriveSessionTitle, in as buildHints, jn as DiscoveryProvider, jt as pendingInteractionsFromTurns, k as useSafeModeActions, kn as quoteForShell, kt as isModelInteraction, l as useSurfaces, li as loadState, ln as SETTINGS_CATEGORIES, lt as buildVisibleMcpRows, m as userScopeOverridesLoad, mi as stripSpawnTokensLine, mn as clampFps, ni as compactSummaryEvent, nn as hintsLength, nt as mcpToolsCachePath, o as ThemeProvider, oi as getSessionMeta, on as listProjectFiles, p as renderSystemMdSeed, pi as serverToolResultSummary, pn as SettingsProvider, q as formatPathForCwd, qt as runEndNotice, r as computeTurnAnchors, rn as truncateTrailing, rt as refreshMcpToolsCatalog, s as useColors, si as lastContextSizeFromTurns, sn as useEnabledToggleSet, st as useMcpToolToggleMap, tn as clipHintsToWidth, tt as loadMcpToolsCache, ui as marginTopFor, un as SETTINGS_CHOICES, ur as chatAutoCompactBehavior, ut as indexOfServerRow, v as buildSpawnStartEvent, vi as toolResultText, vt as getMcpAuthStatus, w as writeSessionExport, wt as PRESENT_PLAN_TOOL, x as discoverProjectSkills, y as buildSkillsConfig, yi as updateToolEventOutcomes, yn as resolveChipColor, yr as accentColor, zn as countClaudeSessionFiles, zr as useActiveTodos } from "./approvals-CqqxE5Vh.js";
|
|
24
|
+
import { E as renderDefaultCallbackPage, T as restoreModelOptions, b as modelSupportsReasoning, c as setPreferredCredential, g as getContextWindow, h as enabledModelOptions, l as setProviderCredential, n as applyApiKeyEnv, t as detectAuth, w as piIdOf, y as modelOptionsFor } from "./auth-BdwDROYg.js";
|
|
25
|
+
import { a as connectIpcClient, f as probeSocket, o as connectOrSpawnDaemon, p as selfDaemonSpawnCommand, u as defaultSocketPath } from "./server-DJqp9dIR.js";
|
|
22
26
|
import { createDockerContext } from "./contexts/docker.js";
|
|
23
27
|
import { createTuiStore } from "./session/sqlite.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { At as hintsLength, B as formatPathForCwd, Bt as SETTINGS_TOGGLES, C as addToSafelist, Cn as upsertMcpServer, Ct as makeRequestInteraction, Dn as discoverProjectMcps, Dr as envSection, Dt as useInteractionsQueue, Er as buildPlanSystem, Et as useInteractionsActions, F as formatUsagePercent, Fr as lastContextSizeFromTurns, Ft as useEnabledToggleSet, G as runOAuthLogin, Gr as toolCallPreview, Hn as chatAutoCompactBehavior, Hr as stripSpawnTokensLine, Ht as clampFps, I as mostConstrainedWindow, Ir as listSessionMeta, It as DEFAULT_SETTINGS, J as loadMcpToolsCache, Jr as usageByModel, K as supportsOAuth, Kr as toolResultText, Kt as resolveChipColor, L as providerUsageSupported, Lr as loadState, Lt as SETTINGS_CATEGORIES, M as clearProviderUsageCache, Mn as buildUpdateHint, Mt as buildHints, N as fetchAllProviderUsage, Nn as useUpdateCheck, Nr as deriveSessionTitle, Or as chatReferenceTokensForPrompt, Ot as EMPTY_HINTS, P as fetchProviderUsage, Pr as eventsFromTurns, Pt as listProjectFiles, Q as subscribeMcpToolsCache, R as usageChecksOptedOut, Rr as marginTopFor, Rt as SETTINGS_CHOICES, Sn as removeMcpServer, St as isModelInteraction, T as isOnSafelist, Tn as dedupMcpServersByName, Tr as buildBuildSystem, U as redirectUriFromAuthUrl, Un as discoverAgentsMd, Ur as sumRunCosts, Ut as useSettings, V as fetchOAuthRedirect, Vr as serverToolResultSummary, Vt as SettingsProvider, W as oauthUsesManualCodePaste, X as refreshMcpToolsCatalog, Y as mcpToolsCachePath, Zn as accentColor, _n as useCompletion, _t as PRESENT_PLAN_TOOL, a as useSurfaces, an as ConfigProvider, at as createFileMcpCredentialStore, b as useSafeModeActions, bn as importFromClaudeCode, bt as createInteractionTools, c as discoverSystemMd, cn as resolveStoragePaths, ct as McpAuthProvider, d as userSystemMdPath, dn as chainsEqual, dt as getMcpAuthStatus, et as useMcpToolToggleMap, f as useStreamBuffer, fn as filterChainFallbacks, fr as setTodosForRun, gn as toggleChainEntry, gt as InteractionsProvider, h as discoverProjectSkills, ht as HOST_INTERACTION_ID_PREFIX, i as useSelectStyle, in as useDiscoveryOptional, it as parentServerName, jn as bootTick, jr as compactSummaryEvent, jt as truncateTrailing, k as suggestSafelistEntries, kn as tryOpenBrowser, kr as createChatTurnsReadTool, kt as clipHintsToWidth, l as renderSystemMdSeed, ln as buildRunModel, lt as useMcpAuthDispatch, m as defaultSkillScanPaths, mn as restoreChainFallbacks, mr as useActiveTodos, n as ThemeProvider, nn as DiscoveryProvider, nt as buildVisibleMcpRows, on as useConfig, p as buildSkillsConfig, pn as providerKeySetChanged, pr as todoProgressLabel, pt as ASK_USER_TOOL, qr as updateToolEventOutcomes, qt as resolveTheme, r as useColors, rn as useDiscovery, rr as TODO_STATUS_GLYPHS, rt as indexOfServerRow, s as useTheme, sn as resolveConfig, t as computeTurnAnchors, tn as createDiscoverySlot, u as userScopeOverridesLoad, un as chainPositionOf, ur as pruneTodosByRun, ut as useMcpAuthState, v as writeSessionExport, vn as countClaudeSessionFiles, vt as buildResumedToolResultsTurn, w as getSafelist, wn as buildMcpServers, wt as pendingInteractionsFromTurns, x as useSafeModeQueue, xn as mcpsWritePath, y as SafeModeProvider, yn as hasClaudeCodeSetup, yt as createAutomaticInteractionResponse, zr as noticeTurnContent, zt as SETTINGS_INPUTS } from "./transcript-anchors-BjH-68k2.js";
|
|
27
|
-
import { basename, delimiter, dirname, join, relative, resolve } from "node:path";
|
|
28
|
+
import { _ as buildVisibleAuthRows, a as editTodo, b as parentProviderKey, c as NOTICE_GLYPH, f as validateMcpForm, g as instructionPreviewLines, h as instructionOriginBadge, i as cycleTodoStatus, l as MCP_TRANSPORTS, m as buildVisibleInstructionRows, n as buildTodoRetryMessage, r as clearTodos, s as removeTodo, t as addTodo, v as indexOfProviderRow, x as selectableSources } from "./todo-edits-Dw2oO2ON.js";
|
|
29
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
28
30
|
import { Buffer } from "node:buffer";
|
|
29
31
|
import * as fs from "node:fs";
|
|
30
|
-
import {
|
|
31
|
-
import { readFile,
|
|
32
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
33
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
32
34
|
import { spawn, spawnSync } from "node:child_process";
|
|
33
35
|
import { homedir, tmpdir } from "node:os";
|
|
34
36
|
import { Fzf, byLengthAsc } from "fzf";
|
|
@@ -36,200 +38,15 @@ import * as React from "react";
|
|
|
36
38
|
import { Component, createContext, createElement, memo, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
37
39
|
import * as ReactJsxRuntime from "react/jsx-runtime";
|
|
38
40
|
import { jsx } from "react/jsx-runtime";
|
|
41
|
+
import process$1 from "node:process";
|
|
39
42
|
import * as OpenTuiCore from "@opentui/core";
|
|
40
|
-
import { BoxRenderable, CodeRenderable, RGBA, SyntaxStyle, TextRenderable, addDefaultParsers, createCliRenderer, decodePasteBytes, defaultTextareaKeyBindings, getTreeSitterClient, parseColor, stripAnsiSequences } from "@opentui/core";
|
|
43
|
+
import { BoxRenderable, CodeRenderable, RGBA, SyntaxStyle, TextAttributes, TextRenderable, addDefaultParsers, createCliRenderer, decodePasteBytes, defaultTextareaKeyBindings, getTreeSitterClient, parseColor, stripAnsiSequences } from "@opentui/core";
|
|
41
44
|
import * as OpenTuiReact from "@opentui/react";
|
|
42
45
|
import { createRoot, useKeyboard, useRenderer, useSelectionHandler, useTerminalDimensions } from "@opentui/react";
|
|
43
46
|
import * as OpenTuiReactJsxDevRuntime from "@opentui/react/jsx-dev-runtime";
|
|
44
47
|
import * as OpenTuiReactJsxRuntime from "@opentui/react/jsx-runtime";
|
|
45
48
|
import { Fragment, jsx as jsx$1, jsxs } from "@opentui/react/jsx-runtime";
|
|
46
49
|
import * as ReactJsxDevRuntime from "react/jsx-dev-runtime";
|
|
47
|
-
//#region src/chat/plugins-discovery.ts
|
|
48
|
-
/** Subdirectory each search root is probed for. */
|
|
49
|
-
const PLUGINS_SUBPATH = "plugins";
|
|
50
|
-
/**
|
|
51
|
-
* Resolve the plugin search roots. First-found wins on name collision;
|
|
52
|
-
* earlier entries take precedence.
|
|
53
|
-
*
|
|
54
|
-
* Search order — see {@link projectUserPaths}:
|
|
55
|
-
*
|
|
56
|
-
* 1. `{project}/.agents/plugins`
|
|
57
|
-
* 2. `{project}/.zidane/plugins`
|
|
58
|
-
* 3. `~/.agents/plugins`
|
|
59
|
-
* 4. `~/.zidane/plugins`
|
|
60
|
-
*
|
|
61
|
-
* Non-existent paths are returned; the walk skips them.
|
|
62
|
-
*
|
|
63
|
-
* Unlike skills and MCP servers there is no foreign-agent or Ruler variant:
|
|
64
|
-
* Agent Plugins is a vendor-neutral format, so `.agents/plugins` IS the
|
|
65
|
-
* cross-tool location. Nothing to import from a competitor's directory.
|
|
66
|
-
*/
|
|
67
|
-
function defaultPluginScanPaths(opts = {}) {
|
|
68
|
-
return projectUserPaths({
|
|
69
|
-
subPath: PLUGINS_SUBPATH,
|
|
70
|
-
...opts
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Parse a platform-delimited list of additional plugin scan roots. Mirrors
|
|
75
|
-
* `parseExtensionScanPathEnv` in `../extensions/discovery`:
|
|
76
|
-
*
|
|
77
|
-
* ZIDANE_PLUGIN_DIRS=/repo/plugins bun run tui
|
|
78
|
-
*
|
|
79
|
-
* Each path is a scan ROOT whose immediate children are plugin directories
|
|
80
|
-
* (`<root>/<name>/plugin.json`), not a single plugin directory.
|
|
81
|
-
*
|
|
82
|
-
* This exists because the four default roots are all dot-directories, and
|
|
83
|
-
* `.zidane/` is gitignored — so a repo has nowhere TRACKED to keep example or
|
|
84
|
-
* first-party plugins for local dogfooding. Extensions solved the same
|
|
85
|
-
* problem the same way.
|
|
86
|
-
*/
|
|
87
|
-
function parsePluginScanPathEnv(value = process.env.ZIDANE_PLUGIN_DIRS) {
|
|
88
|
-
if (!value) return [];
|
|
89
|
-
return value.split(delimiter).map((p) => p.trim()).filter(Boolean);
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Load every plugin reachable from the default search roots.
|
|
93
|
-
*
|
|
94
|
-
* Each immediate child directory of a root is one plugin — the same
|
|
95
|
-
* depth-1 rule the spec applies inside `skills/`. A child that isn't a
|
|
96
|
-
* directory, or that holds no `plugin.json`, is silently skipped: dropping a
|
|
97
|
-
* README next to your plugins is not an error.
|
|
98
|
-
*
|
|
99
|
-
* Dedup is by plugin NAME, not directory name, because the manifest owns
|
|
100
|
-
* identity — two directories called `tools/` shipping different plugins are
|
|
101
|
-
* two plugins, and one plugin vendored at two scopes is one plugin.
|
|
102
|
-
*/
|
|
103
|
-
async function discoverProjectPlugins(opts = {}) {
|
|
104
|
-
const roots = [...defaultPluginScanPaths(opts), ...(opts.extraRoots ?? []).map((path) => ({
|
|
105
|
-
path,
|
|
106
|
-
source: "project"
|
|
107
|
-
}))].filter((p) => existsSync(p.path));
|
|
108
|
-
const dataRoot = defaultPluginDataRoot({
|
|
109
|
-
...opts.prefix !== void 0 ? { prefix: opts.prefix } : {},
|
|
110
|
-
...opts.home !== void 0 ? { home: opts.home } : {}
|
|
111
|
-
});
|
|
112
|
-
const plugins = [];
|
|
113
|
-
const errors = [];
|
|
114
|
-
const seen = /* @__PURE__ */ new Set();
|
|
115
|
-
for (const root of roots) {
|
|
116
|
-
if (opts.signal?.aborted) break;
|
|
117
|
-
let entries;
|
|
118
|
-
try {
|
|
119
|
-
entries = await readdir(root.path);
|
|
120
|
-
} catch (err) {
|
|
121
|
-
errors.push({
|
|
122
|
-
path: root.path,
|
|
123
|
-
source: root.source,
|
|
124
|
-
message: `Could not read plugin directory: ${err instanceof Error ? err.message : String(err)}`
|
|
125
|
-
});
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
for (const entry of entries.sort()) {
|
|
129
|
-
if (opts.signal?.aborted) break;
|
|
130
|
-
const dir = join(root.path, entry);
|
|
131
|
-
try {
|
|
132
|
-
if (!(await stat(dir)).isDirectory()) continue;
|
|
133
|
-
} catch {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
const loaded = await loadAgentPlugin(dir, {
|
|
137
|
-
source: root.source,
|
|
138
|
-
dataRoot
|
|
139
|
-
});
|
|
140
|
-
if (!loaded.ok && loaded.diagnostics.some((d) => d.code === "manifest-missing")) continue;
|
|
141
|
-
const name = loaded.manifest?.name ?? basename(dir);
|
|
142
|
-
if (seen.has(name)) continue;
|
|
143
|
-
seen.add(name);
|
|
144
|
-
plugins.push({
|
|
145
|
-
name,
|
|
146
|
-
plugin: loaded,
|
|
147
|
-
source: root.source,
|
|
148
|
-
path: dir
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
plugins,
|
|
154
|
-
errors
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Map the user's enabled-plugin list onto the contributions a host merges
|
|
159
|
-
* into the agent.
|
|
160
|
-
*
|
|
161
|
-
* Conventions match `enabledSkills` / `enabledMcps`:
|
|
162
|
-
* - `undefined` → every discovered plugin is enabled (default)
|
|
163
|
-
* - `[]` → all off
|
|
164
|
-
* - `[names]` → allowlist
|
|
165
|
-
*
|
|
166
|
-
* A plugin that failed to load contributes nothing regardless of the
|
|
167
|
-
* allowlist — `ok: false` means components were never discovered.
|
|
168
|
-
*/
|
|
169
|
-
function buildPluginContributions(opts) {
|
|
170
|
-
const allow = opts.enabled === void 0 ? null : new Set(opts.enabled);
|
|
171
|
-
const skills = [];
|
|
172
|
-
const mcpServers = [];
|
|
173
|
-
for (const entry of opts.discovered) {
|
|
174
|
-
if (!entry.plugin.ok) continue;
|
|
175
|
-
if (allow !== null && !allow.has(entry.name)) continue;
|
|
176
|
-
skills.push(...entry.plugin.skills);
|
|
177
|
-
mcpServers.push(...entry.plugin.mcpServers);
|
|
178
|
-
}
|
|
179
|
-
return {
|
|
180
|
-
skills,
|
|
181
|
-
mcpServers
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
//#endregion
|
|
185
|
-
//#region src/mcp/warn-log.ts
|
|
186
|
-
/**
|
|
187
|
-
* Subscribe to `mcp:warn`
|
|
188
|
-
* (stdio child stderr lines, see `attachStderrWarnPump` in ./index.ts)
|
|
189
|
-
* and append each to `<dir>/warnings.log`.
|
|
190
|
-
*
|
|
191
|
-
* The stream opens lazily on the first warning so
|
|
192
|
-
* a healthy session never touches disk.
|
|
193
|
-
*/
|
|
194
|
-
function attachMcpWarnLog(hooks, opts) {
|
|
195
|
-
let stream = null;
|
|
196
|
-
let logger = null;
|
|
197
|
-
const ensureLogger = () => {
|
|
198
|
-
if (logger) return logger;
|
|
199
|
-
try {
|
|
200
|
-
mkdirSync(opts.dir, { recursive: true });
|
|
201
|
-
const s = createWriteStream(join(opts.dir, "warnings.log"), { flags: "a" });
|
|
202
|
-
s.on("error", () => {});
|
|
203
|
-
stream = s;
|
|
204
|
-
logger = createLogger(consoleSink({
|
|
205
|
-
stream: s,
|
|
206
|
-
minLevel: "warn"
|
|
207
|
-
}));
|
|
208
|
-
return logger;
|
|
209
|
-
} catch {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
const unhook = hooks.hook("mcp:warn", ({ name, message }) => {
|
|
214
|
-
ensureLogger()?.warn("mcp warn", {
|
|
215
|
-
server: name,
|
|
216
|
-
message
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
return { close: async () => {
|
|
220
|
-
unhook();
|
|
221
|
-
const s = stream;
|
|
222
|
-
stream = null;
|
|
223
|
-
logger = null;
|
|
224
|
-
if (!s) return;
|
|
225
|
-
await new Promise((resolve) => {
|
|
226
|
-
s.once("close", () => resolve());
|
|
227
|
-
s.once("error", () => resolve());
|
|
228
|
-
s.end();
|
|
229
|
-
});
|
|
230
|
-
} };
|
|
231
|
-
}
|
|
232
|
-
//#endregion
|
|
233
50
|
//#region src/tui/clickable.tsx
|
|
234
51
|
/** Glyph / word aliases → OpenTUI key names (`parse.keypress` conventions). */
|
|
235
52
|
const KEY_NAME_ALIASES = {
|
|
@@ -835,35 +652,6 @@ function runBashStreaming(opts) {
|
|
|
835
652
|
});
|
|
836
653
|
}
|
|
837
654
|
//#endregion
|
|
838
|
-
//#region src/tui/builtin-commands.ts
|
|
839
|
-
function createBuiltinCommands(opts) {
|
|
840
|
-
const commands = [{
|
|
841
|
-
name: "help",
|
|
842
|
-
description: "List the available / commands",
|
|
843
|
-
run: (ctx) => {
|
|
844
|
-
const all = [...opts.getCommands()].sort((a, b) => a.name.localeCompare(b.name));
|
|
845
|
-
const body = all.length === 0 ? "_No commands are registered._" : all.map((c) => `- **/${c.name}** — ${c.description}`).join("\n");
|
|
846
|
-
ctx.emit({ text: `### Commands\n\n${body}` });
|
|
847
|
-
}
|
|
848
|
-
}];
|
|
849
|
-
if (opts.reloadExtensions) {
|
|
850
|
-
const reload = opts.reloadExtensions;
|
|
851
|
-
commands.push({
|
|
852
|
-
name: "reload",
|
|
853
|
-
description: "Reload extensions (re-discover, re-run setup, tear down the old instances)",
|
|
854
|
-
run: async (ctx) => {
|
|
855
|
-
ctx.emit({
|
|
856
|
-
text: "",
|
|
857
|
-
detail: { label: "reloading extensions…" }
|
|
858
|
-
});
|
|
859
|
-
const { active, inactive } = await reload();
|
|
860
|
-
ctx.emit({ text: `Reloaded — **${active}** active${inactive > 0 ? `, ${inactive} inactive` : ""}. Agent-side contributions apply on the next run.` });
|
|
861
|
-
}
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
return commands;
|
|
865
|
-
}
|
|
866
|
-
//#endregion
|
|
867
655
|
//#region src/tui/cancel-tool-modal.tsx
|
|
868
656
|
/** @jsxImportSource @opentui/react */
|
|
869
657
|
function CancelToolModal({ inFlight, onCancel, onCancelAll, onClose }) {
|
|
@@ -1746,6 +1534,68 @@ function RenderCall({ render }) {
|
|
|
1746
1534
|
return /* @__PURE__ */ jsx$1(Fragment, { children: render() });
|
|
1747
1535
|
}
|
|
1748
1536
|
//#endregion
|
|
1537
|
+
//#region src/tui/session-titles.ts
|
|
1538
|
+
/**
|
|
1539
|
+
* Session id → title/directory lookup for transcript rows that address ANOTHER
|
|
1540
|
+
* session (the main's `sessions` tool calls). Same external-store shape as
|
|
1541
|
+
* `live-settings.ts`: the app publishes what it learns (every sessions
|
|
1542
|
+
* refresh, every session-event), leaf components subscribe.
|
|
1543
|
+
*
|
|
1544
|
+
* Titles the app has not seen yet — a worker the main created in a
|
|
1545
|
+
* directory outside the current project scope — are fetched once through
|
|
1546
|
+
* the resolver the app installs (a by-id header read against the store),
|
|
1547
|
+
* so the row upgrades from short id to title as soon as the read lands.
|
|
1548
|
+
*/
|
|
1549
|
+
const refs = /* @__PURE__ */ new Map();
|
|
1550
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1551
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
1552
|
+
let resolver = null;
|
|
1553
|
+
function notify() {
|
|
1554
|
+
for (const listener of listeners) listener();
|
|
1555
|
+
}
|
|
1556
|
+
function publishSessionRefs(entries) {
|
|
1557
|
+
let changed = false;
|
|
1558
|
+
for (const { id, title, projectRoot } of entries) {
|
|
1559
|
+
const dir = sessionDirLabel(projectRoot);
|
|
1560
|
+
const prev = refs.get(id);
|
|
1561
|
+
if (prev?.title !== title || prev?.dir !== dir) {
|
|
1562
|
+
refs.set(id, dir ? {
|
|
1563
|
+
title,
|
|
1564
|
+
dir
|
|
1565
|
+
} : { title });
|
|
1566
|
+
changed = true;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (changed) notify();
|
|
1570
|
+
}
|
|
1571
|
+
/** Installed by the app once the store is known; `null` disables lazy lookups. */
|
|
1572
|
+
function setSessionRefResolver(next) {
|
|
1573
|
+
resolver = next;
|
|
1574
|
+
}
|
|
1575
|
+
function subscribe(listener) {
|
|
1576
|
+
listeners.add(listener);
|
|
1577
|
+
return () => {
|
|
1578
|
+
listeners.delete(listener);
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
function requestRef(id) {
|
|
1582
|
+
if (!resolver || refs.has(id) || inFlight.has(id)) return;
|
|
1583
|
+
inFlight.add(id);
|
|
1584
|
+
resolver(id).then((ref) => {
|
|
1585
|
+
if (!ref) return;
|
|
1586
|
+
refs.set(id, ref);
|
|
1587
|
+
notify();
|
|
1588
|
+
}).catch(() => {}).finally(() => inFlight.delete(id));
|
|
1589
|
+
}
|
|
1590
|
+
/** The session's title + directory, or `undefined` until known. Kicks a lazy lookup on a miss. */
|
|
1591
|
+
function useSessionRef(id) {
|
|
1592
|
+
const ref = useSyncExternalStore(subscribe, () => id ? refs.get(id) : void 0, () => id ? refs.get(id) : void 0);
|
|
1593
|
+
useEffect(() => {
|
|
1594
|
+
if (id && ref === void 0) requestRef(id);
|
|
1595
|
+
}, [id, ref]);
|
|
1596
|
+
return ref;
|
|
1597
|
+
}
|
|
1598
|
+
//#endregion
|
|
1749
1599
|
//#region src/tui/terminal-sanitize.ts
|
|
1750
1600
|
/**
|
|
1751
1601
|
* Terminal-display sanitation.
|
|
@@ -1995,7 +1845,7 @@ const BASH_RESULT_MAX_LINES = 10;
|
|
|
1995
1845
|
* same-turn events read as one continuous highlighted block instead of a
|
|
1996
1846
|
* striped list.
|
|
1997
1847
|
*/
|
|
1998
|
-
const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, anchorId, hideChildLabel = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false }) => {
|
|
1848
|
+
const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, anchorId, hideChildLabel = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false, onOpenSessionEvent }) => {
|
|
1999
1849
|
const SURFACE = useSurfaces();
|
|
2000
1850
|
const gap = marginTopFor(event, previous);
|
|
2001
1851
|
const highlighted = selected;
|
|
@@ -2018,7 +1868,8 @@ const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, an
|
|
|
2018
1868
|
settleCodeEpoch,
|
|
2019
1869
|
liveSpawn,
|
|
2020
1870
|
searchQuery,
|
|
2021
|
-
searchTitleMatch
|
|
1871
|
+
searchTitleMatch,
|
|
1872
|
+
onOpenSessionEvent
|
|
2022
1873
|
})
|
|
2023
1874
|
});
|
|
2024
1875
|
});
|
|
@@ -2037,7 +1888,7 @@ function onInputSubmit(handler) {
|
|
|
2037
1888
|
* Width tiering is driven by plain-text length estimates — close enough
|
|
2038
1889
|
* since the segments are ASCII-heavy.
|
|
2039
1890
|
*/
|
|
2040
|
-
function Footer({ hints, context, cost = null, status = null, compactionTokens = null, compactionStartedAt = null, retry = null, contextKey = null, usage = null, usageKey = null }) {
|
|
1891
|
+
function Footer({ hints, context, cost = null, status = null, compactionTokens = null, compactionStartedAt = null, retry = null, contextKey = null, usage = null, usageKey = null, daemonHealth = null }) {
|
|
2041
1892
|
const { width } = useTerminalDimensions();
|
|
2042
1893
|
const SURFACE = useSurfaces();
|
|
2043
1894
|
const showCost = typeof cost === "number" && cost > 0;
|
|
@@ -2054,9 +1905,9 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
|
|
|
2054
1905
|
const timer = setInterval(() => setCompactTick((t) => t + 1), 1e3);
|
|
2055
1906
|
return () => clearInterval(timer);
|
|
2056
1907
|
}, [status]);
|
|
2057
|
-
const statusLabel = status === "compacting" ? compactingLabel(compactionTokens, compactionStartedAt) : status === "searching" ? " Searching messages…" : status === "retrying" ? retryingLabel(retry) : "";
|
|
1908
|
+
const statusLabel = status === "compacting" ? ` ${compactingLabel(compactionTokens, compactionStartedAt)}` : status === "searching" ? " Searching messages…" : status === "retrying" ? retryingLabel(retry) : "";
|
|
2058
1909
|
const sW = status === "compacting" || status === "searching" || status === "retrying" ? 2 + statusLabel.length : status === "asking" ? 3 : status ? 2 : 0;
|
|
2059
|
-
if (hW + sW + (rightW > 0 ? rightW + 1 : 0) <= inner) return /* @__PURE__ */ jsxs("box", {
|
|
1910
|
+
if ((daemonHealth ? 2 : 0) + hW + sW + (rightW > 0 ? rightW + 1 : 0) <= inner) return /* @__PURE__ */ jsxs("box", {
|
|
2060
1911
|
style: {
|
|
2061
1912
|
flexDirection: "row",
|
|
2062
1913
|
height: 1,
|
|
@@ -2065,6 +1916,7 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
|
|
|
2065
1916
|
backgroundColor: SURFACE.background
|
|
2066
1917
|
},
|
|
2067
1918
|
children: [
|
|
1919
|
+
/* @__PURE__ */ jsx$1(DaemonHealthDot, { health: daemonHealth }),
|
|
2068
1920
|
/* @__PURE__ */ jsx$1(HintsText, { hints }),
|
|
2069
1921
|
/* @__PURE__ */ jsx$1(FooterStatusIcon, {
|
|
2070
1922
|
status,
|
|
@@ -2093,10 +1945,14 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
|
|
|
2093
1945
|
flexDirection: "row",
|
|
2094
1946
|
height: 1
|
|
2095
1947
|
},
|
|
2096
|
-
children: [
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
1948
|
+
children: [
|
|
1949
|
+
/* @__PURE__ */ jsx$1(DaemonHealthDot, { health: daemonHealth }),
|
|
1950
|
+
/* @__PURE__ */ jsx$1(HintsText, { hints: stackedHints }),
|
|
1951
|
+
/* @__PURE__ */ jsx$1(FooterStatusIcon, {
|
|
1952
|
+
status,
|
|
1953
|
+
label: statusLabel
|
|
1954
|
+
})
|
|
1955
|
+
]
|
|
2100
1956
|
}), rightW > 0 && /* @__PURE__ */ jsxs("box", {
|
|
2101
1957
|
style: {
|
|
2102
1958
|
flexDirection: "row",
|
|
@@ -2183,6 +2039,25 @@ function ClickableStatus({ keySpec, render }) {
|
|
|
2183
2039
|
* compacting adds a short label because background compaction can otherwise
|
|
2184
2040
|
* look like an idle spinner.
|
|
2185
2041
|
*/
|
|
2042
|
+
/**
|
|
2043
|
+
* The daemon health dot — bottom-left of the footer, two cells (`● `).
|
|
2044
|
+
*
|
|
2045
|
+
* Minimal on purpose: a single glyph in a theme slot, no label. The three
|
|
2046
|
+
* states are told apart by hue alone (green / amber / red via `accent` /
|
|
2047
|
+
* `warn` / `error`), which is enough for a status light and keeps the hints
|
|
2048
|
+
* beside it undisturbed. Renders nothing when `health` is `null` (feature
|
|
2049
|
+
* off), so the footer's width budget is only spent while the daemon is on.
|
|
2050
|
+
*/
|
|
2051
|
+
function DaemonHealthDot({ health }) {
|
|
2052
|
+
const COLOR = useColors();
|
|
2053
|
+
if (!health) return null;
|
|
2054
|
+
const { tone } = DAEMON_HEALTH_STYLE[health];
|
|
2055
|
+
return /* @__PURE__ */ jsx$1("text", {
|
|
2056
|
+
wrapMode: "none",
|
|
2057
|
+
fg: tone === "accent" ? COLOR.accent : tone === "warn" ? COLOR.warn : COLOR.error,
|
|
2058
|
+
children: `● `
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2186
2061
|
function FooterStatusIcon({ status, label = "" }) {
|
|
2187
2062
|
const COLOR = useColors();
|
|
2188
2063
|
if (!status) return null;
|
|
@@ -2204,19 +2079,6 @@ function FooterStatusIcon({ status, label = "" }) {
|
|
|
2204
2079
|
function retryingLabel(retry) {
|
|
2205
2080
|
return retry ? ` Retrying (${retry.attempt}/${retry.max})...` : " Retrying...";
|
|
2206
2081
|
}
|
|
2207
|
-
/**
|
|
2208
|
-
* Footer label for the compacting state — ` Compacting...` plus liveness:
|
|
2209
|
-
* a `~2.1k` output-token estimate once the summary streams, and an elapsed
|
|
2210
|
-
* suffix while it doesn't (prompt ingestion is minutes of silence on
|
|
2211
|
-
* near-window sessions). Kept as a plain string so the Footer's width
|
|
2212
|
-
* math and the rendered spans can't drift apart.
|
|
2213
|
-
*/
|
|
2214
|
-
function compactingLabel(estTokens, startedAt) {
|
|
2215
|
-
const base = " Compacting...";
|
|
2216
|
-
const elapsedMs = typeof startedAt === "number" ? Date.now() - startedAt : 0;
|
|
2217
|
-
const elapsed = elapsedMs >= 1e3 ? ` · ${formatDuration(elapsedMs)}` : "";
|
|
2218
|
-
return typeof estTokens === "number" && estTokens > 0 ? `${base} ~${fmtTokens(estTokens)}${elapsed}` : `${base}${elapsed}`;
|
|
2219
|
-
}
|
|
2220
2082
|
function HintsText({ hints }) {
|
|
2221
2083
|
return /* @__PURE__ */ jsx$1(HintRow, { hints });
|
|
2222
2084
|
}
|
|
@@ -2292,7 +2154,8 @@ const USAGE_GLYPH = "▰";
|
|
|
2292
2154
|
*/
|
|
2293
2155
|
function UsageIndicator({ usage, accent = false }) {
|
|
2294
2156
|
const COLOR = useColors();
|
|
2295
|
-
const
|
|
2157
|
+
const tone = usageTone(usage.usedPercent);
|
|
2158
|
+
const color = accent ? COLOR.accent : tone === "error" ? COLOR.error : tone === "warn" ? COLOR.warn : COLOR.dim;
|
|
2296
2159
|
return /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx$1("span", {
|
|
2297
2160
|
fg: accent ? COLOR.accent : COLOR.mute,
|
|
2298
2161
|
children: `${USAGE_GLYPH} ${usage.label} `
|
|
@@ -2516,7 +2379,7 @@ function costIndicatorLength(cost) {
|
|
|
2516
2379
|
function usageIndicatorLength(usage) {
|
|
2517
2380
|
return 2 + usage.label.length + 1 + footerUsagePercent(usage.usedPercent).length + 1;
|
|
2518
2381
|
}
|
|
2519
|
-
const SPINNER_FRAMES = [
|
|
2382
|
+
const SPINNER_FRAMES$1 = [
|
|
2520
2383
|
"▰▱▱▱▱",
|
|
2521
2384
|
"▰▰▱▱▱",
|
|
2522
2385
|
"▰▰▰▱▱",
|
|
@@ -2528,24 +2391,24 @@ const SPINNER_FRAMES = [
|
|
|
2528
2391
|
"▱▱▱▱▰",
|
|
2529
2392
|
"▱▱▱▱▱"
|
|
2530
2393
|
];
|
|
2531
|
-
const SPINNER_INTERVAL_MS = 80;
|
|
2394
|
+
const SPINNER_INTERVAL_MS$1 = 80;
|
|
2532
2395
|
/**
|
|
2533
2396
|
* Tick a frame index every {@link SPINNER_INTERVAL_MS}. Pulled out of the
|
|
2534
2397
|
* default `Spinner` body so the title-overlay status icon and any other
|
|
2535
2398
|
* tiny single-glyph spinner can share the same animation cadence without
|
|
2536
2399
|
* each instance running its own `setInterval`.
|
|
2537
2400
|
*/
|
|
2538
|
-
function useSpinnerFrame() {
|
|
2401
|
+
function useSpinnerFrame$1() {
|
|
2539
2402
|
const [frame, setFrame] = useState(0);
|
|
2540
2403
|
useEffect(() => {
|
|
2541
|
-
const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), SPINNER_INTERVAL_MS);
|
|
2404
|
+
const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES$1.length), SPINNER_INTERVAL_MS$1);
|
|
2542
2405
|
return () => clearInterval(id);
|
|
2543
2406
|
}, []);
|
|
2544
|
-
return SPINNER_FRAMES[frame];
|
|
2407
|
+
return SPINNER_FRAMES$1[frame];
|
|
2545
2408
|
}
|
|
2546
2409
|
function Spinner({ label }) {
|
|
2547
2410
|
const COLOR = useColors();
|
|
2548
|
-
const glyph = useSpinnerFrame();
|
|
2411
|
+
const glyph = useSpinnerFrame$1();
|
|
2549
2412
|
return /* @__PURE__ */ jsxs("text", {
|
|
2550
2413
|
fg: COLOR.warn,
|
|
2551
2414
|
children: [glyph, label !== void 0 && /* @__PURE__ */ jsx$1("span", {
|
|
@@ -2563,7 +2426,7 @@ function Spinner({ label }) {
|
|
|
2563
2426
|
function StatusSpinner({ color }) {
|
|
2564
2427
|
return /* @__PURE__ */ jsx$1("span", {
|
|
2565
2428
|
fg: color,
|
|
2566
|
-
children: useSpinnerFrame()
|
|
2429
|
+
children: useSpinnerFrame$1()
|
|
2567
2430
|
});
|
|
2568
2431
|
}
|
|
2569
2432
|
/**
|
|
@@ -2711,7 +2574,7 @@ const NO_SEARCH_MATCHES = {
|
|
|
2711
2574
|
* needs a stable id on `StreamEvent` itself, which is a data-model change
|
|
2712
2575
|
* rather than a rendering one.
|
|
2713
2576
|
*/
|
|
2714
|
-
function Transcript({ events, settings, selectedTurnId = null, busy = false, searchQuery, searchPending = false, interactionPending = false, onLoadEarlier, onSelectTurn, onOpenTurn }) {
|
|
2577
|
+
function Transcript({ events, settings, selectedTurnId = null, busy = false, searchQuery, searchPending = false, interactionPending = false, onLoadEarlier, onSelectTurn, onOpenTurn, onOpenSessionEvent }) {
|
|
2715
2578
|
const COLOR = useColors();
|
|
2716
2579
|
const showThrobber = busy && settings.showThrobber;
|
|
2717
2580
|
const codeSettle = useRef({
|
|
@@ -2936,7 +2799,8 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
|
|
|
2936
2799
|
settleCodeEpoch: codeSettle.current.targets.has(item.event) ? codeSettle.current.epoch : 0,
|
|
2937
2800
|
liveSpawn: live?.[0],
|
|
2938
2801
|
searchQuery,
|
|
2939
|
-
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)
|
|
2802
|
+
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),
|
|
2803
|
+
onOpenSessionEvent
|
|
2940
2804
|
});
|
|
2941
2805
|
const selectActive = selectedTurnId !== null && (onSelectTurn !== void 0 || onOpenTurn !== void 0);
|
|
2942
2806
|
const targetTurn = item.event.turnId && !item.event.childId ? ownership.get(item.event.turnId) ?? item.event.turnId : null;
|
|
@@ -3221,7 +3085,7 @@ function partitionTranscript(events, settings, searchReveal) {
|
|
|
3221
3085
|
function SpawnLifecycleMarker({ row, dim, event, hideChildLabel, live, searchMatch = false }) {
|
|
3222
3086
|
const COLOR = useColors();
|
|
3223
3087
|
const matchColor = resolveChipColor(useSurfaces().chips, "default");
|
|
3224
|
-
const frame = useSpinnerFrame();
|
|
3088
|
+
const frame = useSpinnerFrame$1();
|
|
3225
3089
|
const terminal = event.kind === "spawn-end";
|
|
3226
3090
|
const failed = terminal && (event.text.startsWith("error") || event.text.startsWith("aborted"));
|
|
3227
3091
|
const turns = terminal ? event.spawn?.turns : live?.turns;
|
|
@@ -3309,11 +3173,6 @@ const THINKING_GUTTER_WIDTH = 4;
|
|
|
3309
3173
|
* the ambiguity an aligned gutter cannot absorb.
|
|
3310
3174
|
*/
|
|
3311
3175
|
const NOTICE_LABEL_WIDTH = 15;
|
|
3312
|
-
const NOTICE_GLYPH = {
|
|
3313
|
-
info: "·",
|
|
3314
|
-
warn: "!",
|
|
3315
|
-
error: "✗"
|
|
3316
|
-
};
|
|
3317
3176
|
function indentFor(depth) {
|
|
3318
3177
|
return depth && depth > 0 ? depth * INDENT_PER_DEPTH : 0;
|
|
3319
3178
|
}
|
|
@@ -3377,7 +3236,7 @@ function NoticeRow({ row, notice, text }) {
|
|
|
3377
3236
|
})
|
|
3378
3237
|
});
|
|
3379
3238
|
}
|
|
3380
|
-
function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = false, selected = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false }) {
|
|
3239
|
+
function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = false, selected = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false, onOpenSessionEvent }) {
|
|
3381
3240
|
const COLOR = useColors();
|
|
3382
3241
|
const { settings } = useSettings();
|
|
3383
3242
|
const cleanText = stripTerminalControlChars(event.text);
|
|
@@ -3386,7 +3245,7 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
|
|
|
3386
3245
|
const row = rowStyle(indentFor(settings.hideSubagentOutput && (event.kind === "spawn-start" || event.kind === "spawn-end") ? 0 : effectiveDepth));
|
|
3387
3246
|
const child = isChild(event);
|
|
3388
3247
|
switch (event.kind) {
|
|
3389
|
-
case "separator": return
|
|
3248
|
+
case "separator": return null;
|
|
3390
3249
|
case "user-prompt": return /* @__PURE__ */ jsx$1(UserPromptBlock, {
|
|
3391
3250
|
text: safeText,
|
|
3392
3251
|
refs: event.refs,
|
|
@@ -3456,7 +3315,8 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
|
|
|
3456
3315
|
event,
|
|
3457
3316
|
display: settings.toolCallDisplay === "full" ? "full" : "formatted",
|
|
3458
3317
|
dim: child,
|
|
3459
|
-
searchMatch: searchTitleMatch
|
|
3318
|
+
searchMatch: searchTitleMatch,
|
|
3319
|
+
onOpenSession: onOpenSessionEvent
|
|
3460
3320
|
}), event.tool === "todowrite" && /* @__PURE__ */ jsx$1(TodoInProgressList, {
|
|
3461
3321
|
input: event.input,
|
|
3462
3322
|
dim: child
|
|
@@ -3539,6 +3399,15 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
|
|
|
3539
3399
|
event,
|
|
3540
3400
|
indent: row.paddingLeft
|
|
3541
3401
|
});
|
|
3402
|
+
case "session-event": return /* @__PURE__ */ jsx$1(SessionEventBlock, {
|
|
3403
|
+
event,
|
|
3404
|
+
indent: row.paddingLeft,
|
|
3405
|
+
onOpen: onOpenSessionEvent
|
|
3406
|
+
});
|
|
3407
|
+
case "run-summary": return /* @__PURE__ */ jsx$1(RunSummaryLine, {
|
|
3408
|
+
event,
|
|
3409
|
+
indent: row.paddingLeft
|
|
3410
|
+
});
|
|
3542
3411
|
default: return /* @__PURE__ */ jsx$1("text", { children: safeText });
|
|
3543
3412
|
}
|
|
3544
3413
|
}
|
|
@@ -3608,6 +3477,168 @@ function TaskNotificationBlock({ event, indent }) {
|
|
|
3608
3477
|
})
|
|
3609
3478
|
});
|
|
3610
3479
|
}
|
|
3480
|
+
/** Maps a `SessionEventTone` onto the theme color a `session-event` row paints it in. */
|
|
3481
|
+
function sessionEventToneColor(tone, COLOR) {
|
|
3482
|
+
switch (tone) {
|
|
3483
|
+
case "accent": return COLOR.accent;
|
|
3484
|
+
case "error": return COLOR.error;
|
|
3485
|
+
case "warn": return COLOR.warn;
|
|
3486
|
+
case "dim": return COLOR.dim;
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
/** Icon column width — ` ✓ ` (leading space, glyph, trailing space) — so line 2 can indent to the same column line 1's text starts at. */
|
|
3490
|
+
const SESSION_EVENT_GUTTER_WIDTH = 3;
|
|
3491
|
+
/**
|
|
3492
|
+
* Two-line card for a cross-session event delivered to a MAIN
|
|
3493
|
+
* (orchestrating) session: a worker finished, failed, was cancelled, or is
|
|
3494
|
+
* blocked on a decision.
|
|
3495
|
+
*
|
|
3496
|
+
* Line 1 is the receipt: status glyph, title, verb, then `sessionEventMeta`'s
|
|
3497
|
+
* cells (short id, duration, tokens, turns, cost) — the same content both
|
|
3498
|
+
* the TUI and GUI cards show for a run, just laid out for a terminal. Line 2
|
|
3499
|
+
* is the worker's last message (`sessionEventExcerpt`), dimmed and clamped to
|
|
3500
|
+
* the row's own available width so a long summary can't blow out the
|
|
3501
|
+
* transcript; it is omitted entirely when there is nothing to show.
|
|
3502
|
+
*
|
|
3503
|
+
* `needs-attention` reads deliberately louder — both the glyph and the title
|
|
3504
|
+
* ride `warn` (via `SESSION_EVENT_STATUS_STYLE`'s tone) and line 1 gets an
|
|
3505
|
+
* extra nudge — because it is the one status where a human, not the main,
|
|
3506
|
+
* has to act.
|
|
3507
|
+
*
|
|
3508
|
+
* Clickable (mouse only — this is a transcript row, not a focus target):
|
|
3509
|
+
* hovering underlines the title and appends a dim `→ open` hint so the user
|
|
3510
|
+
* can see the row will navigate; activating it opens the worker session
|
|
3511
|
+
* `sessionEvent.sessionId` points at, same destination as the
|
|
3512
|
+
* `openMainSession` chord's round trip. `sessionEvent` is optional on the
|
|
3513
|
+
* wire type, so a host that didn't attach the structured payload still gets
|
|
3514
|
+
* a readable (inert) line — same fallback as before.
|
|
3515
|
+
*/
|
|
3516
|
+
/**
|
|
3517
|
+
* The per-run receipt — `model · 2m14s · 3 turns · $0.04 · ↑4.2k ↓812`.
|
|
3518
|
+
*
|
|
3519
|
+
* One muted line that never wraps: fitted to the row's width and ellipsized
|
|
3520
|
+
* by the shared formatter, so a narrow pane drops cells from the right rather
|
|
3521
|
+
* than spilling onto a second line and breaking the transcript's rhythm.
|
|
3522
|
+
* Content comes from `formatRunReceiptLine`, the same function the GUI and
|
|
3523
|
+
* replay use, so the line reads identically everywhere.
|
|
3524
|
+
*/
|
|
3525
|
+
function RunSummaryLine({ event, indent }) {
|
|
3526
|
+
const COLOR = useColors();
|
|
3527
|
+
const { width: termWidth } = useTerminalDimensions();
|
|
3528
|
+
const receipt = event.runSummary;
|
|
3529
|
+
if (!receipt) return event.text ? /* @__PURE__ */ jsx$1("box", {
|
|
3530
|
+
style: {
|
|
3531
|
+
paddingLeft: indent,
|
|
3532
|
+
paddingBottom: 1
|
|
3533
|
+
},
|
|
3534
|
+
children: /* @__PURE__ */ jsx$1("text", {
|
|
3535
|
+
wrapMode: "none",
|
|
3536
|
+
fg: COLOR.mute,
|
|
3537
|
+
children: event.text
|
|
3538
|
+
})
|
|
3539
|
+
}) : null;
|
|
3540
|
+
const width = Math.max(0, termWidth - 2 - (indent ?? 0));
|
|
3541
|
+
const cells = fitRunReceiptCells(formatRunReceiptCells(receipt), width, 2);
|
|
3542
|
+
if (cells.length === 0) return null;
|
|
3543
|
+
const tint = (kind) => kind === "model" ? COLOR.model : kind === "turns" ? COLOR.brand : kind === "cost" ? COLOR.accent : kind === "speed" ? COLOR.warn : COLOR.dim;
|
|
3544
|
+
const segmentTint = (kind) => kind === "tokens-in" ? COLOR.brand : COLOR.accent;
|
|
3545
|
+
const renderCell = (cell) => {
|
|
3546
|
+
if (cell.segments) return cell.segments.map((seg, j) => /* @__PURE__ */ jsxs("span", { children: [j > 0 && /* @__PURE__ */ jsx$1("span", { children: " " }), /* @__PURE__ */ jsx$1("span", {
|
|
3547
|
+
fg: segmentTint(seg.kind),
|
|
3548
|
+
children: seg.text
|
|
3549
|
+
})] }, `${seg.kind}-${j}`));
|
|
3550
|
+
return /* @__PURE__ */ jsx$1("span", {
|
|
3551
|
+
fg: tint(cell.kind),
|
|
3552
|
+
children: cell.text
|
|
3553
|
+
});
|
|
3554
|
+
};
|
|
3555
|
+
return /* @__PURE__ */ jsx$1("box", {
|
|
3556
|
+
style: {
|
|
3557
|
+
paddingLeft: indent,
|
|
3558
|
+
paddingBottom: 1
|
|
3559
|
+
},
|
|
3560
|
+
children: /* @__PURE__ */ jsxs("text", {
|
|
3561
|
+
wrapMode: "none",
|
|
3562
|
+
children: [/* @__PURE__ */ jsx$1("span", {
|
|
3563
|
+
fg: COLOR.mute,
|
|
3564
|
+
children: `≡ `
|
|
3565
|
+
}), cells.map((cell, i) => /* @__PURE__ */ jsxs("span", { children: [i > 0 && /* @__PURE__ */ jsx$1("span", {
|
|
3566
|
+
fg: COLOR.mute,
|
|
3567
|
+
children: " · "
|
|
3568
|
+
}), renderCell(cell)] }, `${cell.kind}-${i}`))]
|
|
3569
|
+
})
|
|
3570
|
+
});
|
|
3571
|
+
}
|
|
3572
|
+
function SessionEventBlock({ event, indent, onOpen }) {
|
|
3573
|
+
const COLOR = useColors();
|
|
3574
|
+
const { width: termWidth } = useTerminalDimensions();
|
|
3575
|
+
const [hovered, setHovered] = useState(false);
|
|
3576
|
+
const sessionEvent = event.sessionEvent;
|
|
3577
|
+
if (!sessionEvent) return /* @__PURE__ */ jsx$1("box", {
|
|
3578
|
+
style: { paddingLeft: indent },
|
|
3579
|
+
children: /* @__PURE__ */ jsx$1("text", {
|
|
3580
|
+
fg: COLOR.dim,
|
|
3581
|
+
children: event.text
|
|
3582
|
+
})
|
|
3583
|
+
});
|
|
3584
|
+
const clickable = onOpen !== void 0;
|
|
3585
|
+
const style = SESSION_EVENT_STATUS_STYLE[sessionEvent.status];
|
|
3586
|
+
const tone = sessionEventToneColor(style.tone, COLOR);
|
|
3587
|
+
const meta = sessionEventMeta(sessionEvent);
|
|
3588
|
+
const contentWidth = Math.max(0, termWidth - 2 - (indent ?? 0) - SESSION_EVENT_GUTTER_WIDTH);
|
|
3589
|
+
const excerpt = contentWidth > 0 ? sessionEventExcerpt(sessionEvent, contentWidth) : "";
|
|
3590
|
+
const fixed = ` ${style.verb}`.length + (sessionEvent.status === "needs-attention" ? 18 : 0) + meta.reduce((n, cell) => n + ` · ${cell}`.length, 0) + ` · ${shortSessionId(sessionEvent.sessionId)}`.length + 7;
|
|
3591
|
+
const title = truncateTitle(sessionEvent.title, Math.max(12, contentWidth - fixed));
|
|
3592
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
3593
|
+
style: {
|
|
3594
|
+
paddingLeft: indent,
|
|
3595
|
+
flexDirection: "column"
|
|
3596
|
+
},
|
|
3597
|
+
children: [/* @__PURE__ */ jsxs("text", {
|
|
3598
|
+
wrapMode: "none",
|
|
3599
|
+
selectable: false,
|
|
3600
|
+
onMouseDown: clickable ? (mouseEvent) => {
|
|
3601
|
+
mouseEvent.stopPropagation();
|
|
3602
|
+
mouseEvent.preventDefault();
|
|
3603
|
+
onOpen(sessionEvent.sessionId);
|
|
3604
|
+
} : void 0,
|
|
3605
|
+
onMouseOver: clickable ? () => setHovered(true) : void 0,
|
|
3606
|
+
onMouseOut: clickable ? () => setHovered(false) : void 0,
|
|
3607
|
+
children: [
|
|
3608
|
+
/* @__PURE__ */ jsx$1("span", {
|
|
3609
|
+
fg: tone,
|
|
3610
|
+
children: ` ${style.glyph} `
|
|
3611
|
+
}),
|
|
3612
|
+
/* @__PURE__ */ jsx$1("span", {
|
|
3613
|
+
fg: tone,
|
|
3614
|
+
attributes: hovered ? TextAttributes.BOLD | TextAttributes.UNDERLINE : TextAttributes.BOLD,
|
|
3615
|
+
children: title
|
|
3616
|
+
}),
|
|
3617
|
+
/* @__PURE__ */ jsx$1("span", { children: ` ${style.verb}` }),
|
|
3618
|
+
sessionEvent.status === "needs-attention" && /* @__PURE__ */ jsx$1("span", {
|
|
3619
|
+
fg: COLOR.warn,
|
|
3620
|
+
children: " — click to answer"
|
|
3621
|
+
}),
|
|
3622
|
+
meta.map((cell, i) => /* @__PURE__ */ jsx$1("span", {
|
|
3623
|
+
fg: COLOR.dim,
|
|
3624
|
+
children: ` · ${cell}`
|
|
3625
|
+
}, i)),
|
|
3626
|
+
/* @__PURE__ */ jsx$1("span", {
|
|
3627
|
+
fg: COLOR.mute,
|
|
3628
|
+
children: ` · ${shortSessionId(sessionEvent.sessionId)}`
|
|
3629
|
+
}),
|
|
3630
|
+
hovered && /* @__PURE__ */ jsx$1("span", {
|
|
3631
|
+
fg: COLOR.dim,
|
|
3632
|
+
children: " → open"
|
|
3633
|
+
})
|
|
3634
|
+
]
|
|
3635
|
+
}), excerpt && /* @__PURE__ */ jsx$1("text", {
|
|
3636
|
+
wrapMode: "none",
|
|
3637
|
+
fg: COLOR.dim,
|
|
3638
|
+
children: `${" ".repeat(SESSION_EVENT_GUTTER_WIDTH)}${excerpt}`
|
|
3639
|
+
})]
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3611
3642
|
/**
|
|
3612
3643
|
* Boundary row for `compact-summary` events. The persisted summary still
|
|
3613
3644
|
* drives the provider context, but by default the TUI only shows metadata
|
|
@@ -3640,31 +3671,34 @@ function CompactSummaryBlock({ event, indent }) {
|
|
|
3640
3671
|
meta && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3641
3672
|
/* @__PURE__ */ jsx$1("span", {
|
|
3642
3673
|
fg: COLOR.mute,
|
|
3643
|
-
children: " ·
|
|
3674
|
+
children: " · "
|
|
3644
3675
|
}),
|
|
3645
3676
|
/* @__PURE__ */ jsx$1("span", {
|
|
3646
3677
|
fg: COLOR.model,
|
|
3647
3678
|
children: meta.model
|
|
3648
3679
|
}),
|
|
3680
|
+
meta.costUsd !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
|
|
3681
|
+
fg: COLOR.mute,
|
|
3682
|
+
children: " · "
|
|
3683
|
+
}), /* @__PURE__ */ jsx$1("span", {
|
|
3684
|
+
fg: COLOR.accent,
|
|
3685
|
+
children: formatCostUsd(meta.costUsd)
|
|
3686
|
+
})] }),
|
|
3649
3687
|
/* @__PURE__ */ jsx$1("span", {
|
|
3650
3688
|
fg: COLOR.mute,
|
|
3651
3689
|
children: " · "
|
|
3652
3690
|
}),
|
|
3653
3691
|
/* @__PURE__ */ jsx$1("span", {
|
|
3654
|
-
fg: COLOR.
|
|
3655
|
-
children:
|
|
3692
|
+
fg: COLOR.brand,
|
|
3693
|
+
children: `↑${formatTokenCount(meta.inputTokens + meta.cacheReadTokens + meta.cacheCreationTokens)}`
|
|
3656
3694
|
}),
|
|
3657
3695
|
/* @__PURE__ */ jsx$1("span", {
|
|
3658
3696
|
fg: COLOR.mute,
|
|
3659
|
-
children: "
|
|
3660
|
-
}),
|
|
3661
|
-
/* @__PURE__ */ jsx$1("span", {
|
|
3662
|
-
fg: COLOR.dim,
|
|
3663
|
-
children: fmtTokens(meta.outputTokens)
|
|
3697
|
+
children: " "
|
|
3664
3698
|
}),
|
|
3665
3699
|
/* @__PURE__ */ jsx$1("span", {
|
|
3666
|
-
fg: COLOR.
|
|
3667
|
-
children:
|
|
3700
|
+
fg: COLOR.accent,
|
|
3701
|
+
children: `↓${formatTokenCount(meta.outputTokens)}`
|
|
3668
3702
|
})
|
|
3669
3703
|
] })
|
|
3670
3704
|
]
|
|
@@ -4649,8 +4683,9 @@ function HunkBlock({ index, hunk, outcome, tool, path, filetype, mdStyle, COLOR,
|
|
|
4649
4683
|
});
|
|
4650
4684
|
}
|
|
4651
4685
|
const FULL_VIEW_MAX_INPUT_BYTES = 32 * 1024;
|
|
4652
|
-
function ToolCallBlock({ event, display, dim, searchMatch = false }) {
|
|
4686
|
+
function ToolCallBlock({ event, display, dim, searchMatch = false, onOpenSession }) {
|
|
4653
4687
|
const COLOR = useColors();
|
|
4688
|
+
const [hovered, setHovered] = useState(false);
|
|
4654
4689
|
const matchColor = resolveChipColor(useSurfaces().chips, "default");
|
|
4655
4690
|
const mdStyle = useMdStyle();
|
|
4656
4691
|
const name = event.tool ?? "";
|
|
@@ -4665,6 +4700,8 @@ function ToolCallBlock({ event, display, dim, searchMatch = false }) {
|
|
|
4665
4700
|
}
|
|
4666
4701
|
}, [event.input]);
|
|
4667
4702
|
const line = useMemo(() => event.input ? formatToolCall(name, event.input) : null, [event.input, name]);
|
|
4703
|
+
const sessionRef = useSessionRef(line?.sessionId);
|
|
4704
|
+
const { width: termWidth } = useTerminalDimensions();
|
|
4668
4705
|
if (display === "full") return /* @__PURE__ */ jsxs("box", {
|
|
4669
4706
|
style: {
|
|
4670
4707
|
flexDirection: "column",
|
|
@@ -4703,9 +4740,19 @@ function ToolCallBlock({ event, display, dim, searchMatch = false }) {
|
|
|
4703
4740
|
children: " (no structured input)"
|
|
4704
4741
|
})]
|
|
4705
4742
|
});
|
|
4706
|
-
|
|
4743
|
+
const targetSession = line?.sessionId;
|
|
4744
|
+
const clickable = targetSession !== void 0 && onOpenSession !== void 0;
|
|
4745
|
+
const header = /* @__PURE__ */ jsxs("text", {
|
|
4707
4746
|
fg: dim ? COLOR.dim : COLOR.model,
|
|
4708
4747
|
wrapMode: "none",
|
|
4748
|
+
selectable: !clickable,
|
|
4749
|
+
onMouseDown: clickable ? (mouseEvent) => {
|
|
4750
|
+
mouseEvent.stopPropagation();
|
|
4751
|
+
mouseEvent.preventDefault();
|
|
4752
|
+
onOpenSession(targetSession);
|
|
4753
|
+
} : void 0,
|
|
4754
|
+
onMouseOver: clickable ? () => setHovered(true) : void 0,
|
|
4755
|
+
onMouseOut: clickable ? () => setHovered(false) : void 0,
|
|
4709
4756
|
children: [
|
|
4710
4757
|
/* @__PURE__ */ jsx$1("span", {
|
|
4711
4758
|
fg: COLOR.mute,
|
|
@@ -4716,19 +4763,49 @@ function ToolCallBlock({ event, display, dim, searchMatch = false }) {
|
|
|
4716
4763
|
bg: searchMatch ? matchColor.bg : void 0,
|
|
4717
4764
|
children: verb
|
|
4718
4765
|
}),
|
|
4719
|
-
line?.target && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4766
|
+
line?.target && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4767
|
+
/* @__PURE__ */ jsx$1("span", {
|
|
4768
|
+
fg: COLOR.mute,
|
|
4769
|
+
children: " "
|
|
4770
|
+
}),
|
|
4771
|
+
/* @__PURE__ */ jsx$1("span", {
|
|
4772
|
+
fg: dim ? COLOR.dim : COLOR.warn,
|
|
4773
|
+
attributes: hovered ? TextAttributes.UNDERLINE : void 0,
|
|
4774
|
+
children: sessionRef?.title ?? line.target
|
|
4775
|
+
}),
|
|
4776
|
+
sessionRef?.dir && /* @__PURE__ */ jsx$1("span", {
|
|
4777
|
+
fg: COLOR.mute,
|
|
4778
|
+
children: ` · ${sessionRef.dir}`
|
|
4779
|
+
})
|
|
4780
|
+
] }),
|
|
4726
4781
|
line?.meta && line.meta.length > 0 && /* @__PURE__ */ jsx$1("span", {
|
|
4727
4782
|
fg: COLOR.mute,
|
|
4728
4783
|
children: ` · ${line.meta.join(" · ")}`
|
|
4784
|
+
}),
|
|
4785
|
+
hovered && /* @__PURE__ */ jsx$1("span", {
|
|
4786
|
+
fg: COLOR.dim,
|
|
4787
|
+
children: " → open"
|
|
4729
4788
|
})
|
|
4730
4789
|
]
|
|
4731
4790
|
});
|
|
4791
|
+
if (!line?.detail) return header;
|
|
4792
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
4793
|
+
style: {
|
|
4794
|
+
flexDirection: "column",
|
|
4795
|
+
flexShrink: 0,
|
|
4796
|
+
alignSelf: "stretch"
|
|
4797
|
+
},
|
|
4798
|
+
children: [header, /* @__PURE__ */ jsx$1("text", {
|
|
4799
|
+
wrapMode: "none",
|
|
4800
|
+
fg: COLOR.dim,
|
|
4801
|
+
children: ` ${clipLine(line.detail, Math.max(0, termWidth - 4))}`
|
|
4802
|
+
})]
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4805
|
+
/** Flatten whitespace and clip to `max` columns with a trailing ellipsis. */
|
|
4806
|
+
function clipLine(text, max) {
|
|
4807
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
4808
|
+
return flat.length <= max ? flat : `${flat.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
|
|
4732
4809
|
}
|
|
4733
4810
|
function TodoInProgressList({ input, dim }) {
|
|
4734
4811
|
const COLOR = useColors();
|
|
@@ -6142,94 +6219,6 @@ function readToolsByServer(dataDir) {
|
|
|
6142
6219
|
return out;
|
|
6143
6220
|
}
|
|
6144
6221
|
//#endregion
|
|
6145
|
-
//#region src/tui/editor-command.ts
|
|
6146
|
-
/**
|
|
6147
|
-
* `$EDITOR` / `$VISUAL` command parsing.
|
|
6148
|
-
*
|
|
6149
|
-
* These variables conventionally hold a command LINE, not a bare executable —
|
|
6150
|
-
* `code --wait`, `cursor --wait`, `subl -w`, `emacsclient -nw` are all normal
|
|
6151
|
-
* values. Spawning the whole string as argv[0] looks for a file literally named
|
|
6152
|
-
* `cursor --wait` and fails with ENOENT, so the flags have to be split off.
|
|
6153
|
-
*
|
|
6154
|
-
* Kept separate from `app.tsx` (and free of `node:child_process`) so the
|
|
6155
|
-
* tokenizer is unit-testable without pulling in the TUI.
|
|
6156
|
-
*/
|
|
6157
|
-
/**
|
|
6158
|
-
* Split an editor command line into `[executable, ...args]`.
|
|
6159
|
-
*
|
|
6160
|
-
* Whitespace-separated, with single and double quotes honored so an editor
|
|
6161
|
-
* living under a path with spaces still works
|
|
6162
|
-
* (`"/Applications/My App/bin/ed" -w`). Quotes group and are stripped;
|
|
6163
|
-
* backslash escaping is deliberately NOT implemented, because on Windows
|
|
6164
|
-
* backslash is the path separator and treating `C:\tools\ed` as escapes would
|
|
6165
|
-
* corrupt far more command lines than it would rescue.
|
|
6166
|
-
*
|
|
6167
|
-
* Returns `[]` for an empty or whitespace-only spec, so callers can fall back
|
|
6168
|
-
* to the platform opener rather than spawning `''`.
|
|
6169
|
-
*/
|
|
6170
|
-
function parseEditorCommand(spec) {
|
|
6171
|
-
const tokens = [];
|
|
6172
|
-
let current = "";
|
|
6173
|
-
let quote = null;
|
|
6174
|
-
let quoted = false;
|
|
6175
|
-
for (const ch of spec) {
|
|
6176
|
-
if (quote) {
|
|
6177
|
-
if (ch === quote) quote = null;
|
|
6178
|
-
else current += ch;
|
|
6179
|
-
continue;
|
|
6180
|
-
}
|
|
6181
|
-
if (ch === "\"" || ch === "'") {
|
|
6182
|
-
quote = ch;
|
|
6183
|
-
quoted = true;
|
|
6184
|
-
continue;
|
|
6185
|
-
}
|
|
6186
|
-
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
6187
|
-
if (current !== "" || quoted) {
|
|
6188
|
-
tokens.push(current);
|
|
6189
|
-
current = "";
|
|
6190
|
-
quoted = false;
|
|
6191
|
-
}
|
|
6192
|
-
continue;
|
|
6193
|
-
}
|
|
6194
|
-
current += ch;
|
|
6195
|
-
}
|
|
6196
|
-
if (current !== "" || quoted) tokens.push(current);
|
|
6197
|
-
return tokens;
|
|
6198
|
-
}
|
|
6199
|
-
/**
|
|
6200
|
-
* Re-quote a single argv token for a shell that will re-parse the joined command
|
|
6201
|
-
* line — i.e. `spawn(..., { shell: true })`, which Node uses on Windows so that
|
|
6202
|
-
* `.cmd` / `.bat` wrappers (how `code`, `cursor` and friends ship there) resolve.
|
|
6203
|
-
*
|
|
6204
|
-
* Node does NOT re-quote when `shell` is set: it joins `[cmd, ...args]` with
|
|
6205
|
-
* spaces and hands the string over. Since {@link parseEditorCommand} has already
|
|
6206
|
-
* STRIPPED the user's quotes, handing tokens back unquoted would split
|
|
6207
|
-
* `C:\Program Files\ed.exe` at the space and try to run `C:\Program`. This puts
|
|
6208
|
-
* the quotes back.
|
|
6209
|
-
*
|
|
6210
|
-
* Only tokens that need it are quoted, so a plain `code` stays `code`. A literal
|
|
6211
|
-
* double quote is doubled, which is how `cmd.exe` escapes one inside a quoted
|
|
6212
|
-
* run.
|
|
6213
|
-
*
|
|
6214
|
-
* The test covers `cmd.exe` METACHARACTERS, not just whitespace: `&`, `|`, `^`,
|
|
6215
|
-
* `<`, `>` and friends carry none of the obvious "needs quoting" signals, so a
|
|
6216
|
-
* repo cloned into `C:\work\a&b` would otherwise hand `C:\work\a&b\AGENTS.md`
|
|
6217
|
-
* over bare, and cmd would split it at the `&` and run the tail as a second
|
|
6218
|
-
* command.
|
|
6219
|
-
*
|
|
6220
|
-
* `%` and `!` are in the class so such a token is quoted rather than passed
|
|
6221
|
-
* bare — but be clear about the limit: quoting does NOT stop cmd.exe expanding
|
|
6222
|
-
* `%VAR%`, nor `!VAR!` where delayed expansion is on. Double quotes suppress
|
|
6223
|
-
* splitting and other syntax, not expansion. That is a property of the shell
|
|
6224
|
-
* which no amount of quoting can close; the worst case is a wrong path, not an
|
|
6225
|
-
* injected command.
|
|
6226
|
-
*/
|
|
6227
|
-
function quoteForShell(token) {
|
|
6228
|
-
if (token === "") return "\"\"";
|
|
6229
|
-
if (!/[\s"&|^<>%()!]/.test(token)) return token;
|
|
6230
|
-
return `"${token.replace(/"/g, "\"\"")}"`;
|
|
6231
|
-
}
|
|
6232
|
-
//#endregion
|
|
6233
6222
|
//#region src/tui/extension-modal.ts
|
|
6234
6223
|
/**
|
|
6235
6224
|
* Resolve a modal contribution to a renderable node. Eager nodes pass through;
|
|
@@ -6735,6 +6724,24 @@ function CountsBadge$1({ count }) {
|
|
|
6735
6724
|
}
|
|
6736
6725
|
//#endregion
|
|
6737
6726
|
//#region src/tui/live-settings.ts
|
|
6727
|
+
/**
|
|
6728
|
+
* Process-global mirror of the live `Settings`, backing extension
|
|
6729
|
+
* `ctx.config.get`.
|
|
6730
|
+
*
|
|
6731
|
+
* The boot extension registry is built in `runTui` BEFORE React mounts,
|
|
6732
|
+
* so its per-extension `ctx.config.get` closures can't capture a React
|
|
6733
|
+
* ref — yet they must return the user's CURRENT config value when they
|
|
6734
|
+
* fire inside a hook / tool mid-session. This tiny holder bridges the
|
|
6735
|
+
* gap: `runTui` seeds it from the persisted boot snapshot, then the
|
|
6736
|
+
* mounted `App` keeps it in lock-step with the live `Settings`. Any
|
|
6737
|
+
* registry (boot or a cwd-rebuild) reads through {@link readLiveSettings},
|
|
6738
|
+
* so an extension always sees the latest values regardless of which
|
|
6739
|
+
* registry built its context.
|
|
6740
|
+
*
|
|
6741
|
+
* Singleton is acceptable: the TUI is one app per process. Core tests
|
|
6742
|
+
* never touch this — they pass their own `readSettings` getter to
|
|
6743
|
+
* `buildExtensionRegistry`.
|
|
6744
|
+
*/
|
|
6738
6745
|
/** Mutable holder — written by `runTui` (boot seed) + `App` (live updates). */
|
|
6739
6746
|
const liveSettingsHolder = { current: void 0 };
|
|
6740
6747
|
/** Stable getter handed to `discoverAndBuildExtensionRegistry` as `readSettings`. */
|
|
@@ -6745,6 +6752,58 @@ function readLiveSettings() {
|
|
|
6745
6752
|
function setLiveSettings(settings) {
|
|
6746
6753
|
liveSettingsHolder.current = settings;
|
|
6747
6754
|
}
|
|
6755
|
+
/**
|
|
6756
|
+
* Whether a daemon controller is currently connected (daemon runtime).
|
|
6757
|
+
*
|
|
6758
|
+
* Bridges the same App↔AppShell scope gap as the settings mirror:
|
|
6759
|
+
* `onSettingsChange` lives in the top-level `App` (next to
|
|
6760
|
+
* `SettingsProvider`), but the daemon controller lives in `AppShell`. The
|
|
6761
|
+
* settings writer must persist LOCALLY whenever no daemon is connected
|
|
6762
|
+
* (auth/picker screens, before first activation, disconnect→reconnect
|
|
6763
|
+
* grace) — otherwise the change is only `publishDraft`-style broadcast and
|
|
6764
|
+
* silently lost on restart. AppShell keeps this flag current; `App` reads
|
|
6765
|
+
* it to decide whether the daemon owns the settings file right now.
|
|
6766
|
+
*
|
|
6767
|
+
* Singleton per process, same as the settings mirror.
|
|
6768
|
+
*/
|
|
6769
|
+
/**
|
|
6770
|
+
* Emergency kill-switch for the daemon runtime — wins over the `useDaemon`
|
|
6771
|
+
* setting. Set by `zidane --no-daemon` (the CLI exports the env var so it
|
|
6772
|
+
* works identically in dev and compiled binaries) or directly via
|
|
6773
|
+
* `ZIDANE_NO_DAEMON=1`. Exists so a daemon-path bug can never lock the
|
|
6774
|
+
* user out: the in-process runtime is always one flag away, even when the
|
|
6775
|
+
* settings UI itself is unreachable.
|
|
6776
|
+
*/
|
|
6777
|
+
function isDaemonDisabledByEnv() {
|
|
6778
|
+
return process$1.env.ZIDANE_NO_DAEMON === "1";
|
|
6779
|
+
}
|
|
6780
|
+
let daemonHealth = "disconnected";
|
|
6781
|
+
const daemonHealthListeners = /* @__PURE__ */ new Set();
|
|
6782
|
+
function setDaemonHealth(next) {
|
|
6783
|
+
if (daemonHealth === next) return;
|
|
6784
|
+
daemonHealth = next;
|
|
6785
|
+
for (const listener of daemonHealthListeners) listener();
|
|
6786
|
+
}
|
|
6787
|
+
/** Boolean shorthand kept for the existing call sites. */
|
|
6788
|
+
function setDaemonConnected(connected) {
|
|
6789
|
+
setDaemonHealth(connected ? "connected" : "disconnected");
|
|
6790
|
+
}
|
|
6791
|
+
function isDaemonConnected() {
|
|
6792
|
+
return daemonHealth === "connected";
|
|
6793
|
+
}
|
|
6794
|
+
function getDaemonHealth() {
|
|
6795
|
+
return daemonHealth;
|
|
6796
|
+
}
|
|
6797
|
+
function subscribeDaemonHealth(listener) {
|
|
6798
|
+
daemonHealthListeners.add(listener);
|
|
6799
|
+
return () => {
|
|
6800
|
+
daemonHealthListeners.delete(listener);
|
|
6801
|
+
};
|
|
6802
|
+
}
|
|
6803
|
+
/** React binding over the store above. */
|
|
6804
|
+
function useDaemonHealth() {
|
|
6805
|
+
return useSyncExternalStore(subscribeDaemonHealth, getDaemonHealth, getDaemonHealth);
|
|
6806
|
+
}
|
|
6748
6807
|
//#endregion
|
|
6749
6808
|
//#region src/tui/model-options-picker.tsx
|
|
6750
6809
|
const BASE_LEVELS$1 = [
|
|
@@ -7045,16 +7104,7 @@ function selectedModelsFirst(entries, selectedKeys, keyOf) {
|
|
|
7045
7104
|
function providerModelKey(providerKey, modelId) {
|
|
7046
7105
|
return `${providerKey}\u0000${modelId}`;
|
|
7047
7106
|
}
|
|
7048
|
-
|
|
7049
|
-
function reorderSelectedModel(selected, key, delta, keyOf) {
|
|
7050
|
-
const from = selected.findIndex((entry) => keyOf(entry) === key);
|
|
7051
|
-
const to = from + delta;
|
|
7052
|
-
if (from < 0 || to < 0 || to >= selected.length) return selected;
|
|
7053
|
-
const next = [...selected];
|
|
7054
|
-
const [entry] = next.splice(from, 1);
|
|
7055
|
-
next.splice(to, 0, entry);
|
|
7056
|
-
return next;
|
|
7057
|
-
}
|
|
7107
|
+
const reorderSelectedModel = reorderKeyed;
|
|
7058
7108
|
function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onApplyChain }) {
|
|
7059
7109
|
const COLOR = useColors();
|
|
7060
7110
|
const SURFACE = useSurfaces();
|
|
@@ -7124,7 +7174,7 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
|
|
|
7124
7174
|
offCatalog: true,
|
|
7125
7175
|
searchCorpus: ""
|
|
7126
7176
|
} : null;
|
|
7127
|
-
const commit = () => {
|
|
7177
|
+
const commit = (toggleOnly = false) => {
|
|
7128
7178
|
const row = filtered[safeIndex];
|
|
7129
7179
|
const target = row ? {
|
|
7130
7180
|
provider: row.providerKey,
|
|
@@ -7136,7 +7186,7 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
|
|
|
7136
7186
|
if (!target) return;
|
|
7137
7187
|
if (chainMode) {
|
|
7138
7188
|
if (!row) setTypedEntries((prev) => prev.some((e) => e.provider === target.provider && e.model === target.model) ? prev : [...prev, target]);
|
|
7139
|
-
setChainState((prev) => toggleChainEntry(prev, target));
|
|
7189
|
+
setChainState((prev) => toggleOnly ? toggleChainEntry(prev, target) : pickChainPrimary(prev, target));
|
|
7140
7190
|
return;
|
|
7141
7191
|
}
|
|
7142
7192
|
onPick?.({
|
|
@@ -7198,6 +7248,10 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
|
|
|
7198
7248
|
});
|
|
7199
7249
|
return;
|
|
7200
7250
|
}
|
|
7251
|
+
if (key.name === "tab") {
|
|
7252
|
+
commit(true);
|
|
7253
|
+
return;
|
|
7254
|
+
}
|
|
7201
7255
|
if (key.name === "return") commit();
|
|
7202
7256
|
});
|
|
7203
7257
|
if (providers.length === 0) return /* @__PURE__ */ jsx$1(EmptyProvidersState, {});
|
|
@@ -7260,7 +7314,12 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
|
|
|
7260
7314
|
},
|
|
7261
7315
|
{
|
|
7262
7316
|
key: "↵",
|
|
7263
|
-
label: customEntry ? "use anyway" : chainMode ? "
|
|
7317
|
+
label: customEntry ? "use anyway" : chainMode ? "use as primary" : "select",
|
|
7318
|
+
labelColor: COLOR.dim
|
|
7319
|
+
},
|
|
7320
|
+
chainMode && {
|
|
7321
|
+
key: "⇥",
|
|
7322
|
+
label: "add/remove fallback",
|
|
7264
7323
|
labelColor: COLOR.dim
|
|
7265
7324
|
},
|
|
7266
7325
|
chainMode && {
|
|
@@ -7564,6 +7623,425 @@ function describeModel(m) {
|
|
|
7564
7623
|
return parts.join(" · ");
|
|
7565
7624
|
}
|
|
7566
7625
|
//#endregion
|
|
7626
|
+
//#region src/tui/runtime/daemon.ts
|
|
7627
|
+
/** JSON-RPC "method not found" — the daemon predates an optional feature. */
|
|
7628
|
+
const METHOD_NOT_FOUND = -32601;
|
|
7629
|
+
/**
|
|
7630
|
+
* Run an RPC for an OPTIONAL feature: an older daemon that doesn't know
|
|
7631
|
+
* the method degrades the feature silently instead of failing the caller.
|
|
7632
|
+
* Version-skewed daemons exist by construction (a daemon process is frozen
|
|
7633
|
+
* at spawn time while windows come from newer builds mid-development).
|
|
7634
|
+
*/
|
|
7635
|
+
async function optional(call) {
|
|
7636
|
+
try {
|
|
7637
|
+
return await call();
|
|
7638
|
+
} catch (err) {
|
|
7639
|
+
if (err instanceof JsonRpcRemoteError && err.code === METHOD_NOT_FOUND) return null;
|
|
7640
|
+
if (err instanceof JsonRpcConnectionClosedError) return null;
|
|
7641
|
+
throw err;
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7644
|
+
async function connectDaemonSession(options) {
|
|
7645
|
+
const { sessionId, callbacks } = options;
|
|
7646
|
+
let disposed = false;
|
|
7647
|
+
const client = await connectOrSpawnDaemon({
|
|
7648
|
+
...options.socketPath ? { socketPath: options.socketPath } : {},
|
|
7649
|
+
spawnCommand: options.spawnCommand ?? selfDaemonSpawnCommand(),
|
|
7650
|
+
...options.appVersion ? { appVersion: options.appVersion } : {},
|
|
7651
|
+
onClose: () => {
|
|
7652
|
+
if (!disposed) callbacks.onDisconnect?.();
|
|
7653
|
+
}
|
|
7654
|
+
});
|
|
7655
|
+
let running = false;
|
|
7656
|
+
/** A `run:start` observed via signal wins over the initial RPC probe. */
|
|
7657
|
+
let sawRunStart = false;
|
|
7658
|
+
/** Sequence for `suggest.resolve` — see `resolveSuggestion`. */
|
|
7659
|
+
let suggestSeq = 0;
|
|
7660
|
+
/**
|
|
7661
|
+
* Live `event` signals that arrive between subscribing and delivering
|
|
7662
|
+
* the authoritative `sessions.loadEvents` snapshot are buffered, then
|
|
7663
|
+
* flushed in order once the snapshot lands — so nothing is missed in the
|
|
7664
|
+
* gap and the snapshot never clobbers a live event.
|
|
7665
|
+
*/
|
|
7666
|
+
let transcriptReady = false;
|
|
7667
|
+
const bufferedEvents = [];
|
|
7668
|
+
/**
|
|
7669
|
+
* De-dup HITL surfacing across the backfill + live-signal overlap, and
|
|
7670
|
+
* skip requests another window already resolved.
|
|
7671
|
+
*/
|
|
7672
|
+
const surfacedHitl = /* @__PURE__ */ new Set();
|
|
7673
|
+
const resolvedHitl = /* @__PURE__ */ new Set();
|
|
7674
|
+
const surfaceApproval = (request) => {
|
|
7675
|
+
if (request.sessionId !== sessionId || surfacedHitl.has(request.id) || resolvedHitl.has(request.id)) return;
|
|
7676
|
+
surfacedHitl.add(request.id);
|
|
7677
|
+
callbacks.onApproval(request, (decision) => {
|
|
7678
|
+
if (disposed || resolvedHitl.has(request.id)) return;
|
|
7679
|
+
client.request("approvals.resolve", {
|
|
7680
|
+
requestId: request.id,
|
|
7681
|
+
decision
|
|
7682
|
+
}).catch(() => void 0);
|
|
7683
|
+
});
|
|
7684
|
+
};
|
|
7685
|
+
const surfaceInteraction = (request) => {
|
|
7686
|
+
if (surfacedHitl.has(request.id) || resolvedHitl.has(request.id)) return;
|
|
7687
|
+
surfacedHitl.add(request.id);
|
|
7688
|
+
callbacks.onInteraction(request, {
|
|
7689
|
+
respond: (response) => {
|
|
7690
|
+
if (disposed || resolvedHitl.has(request.id)) return;
|
|
7691
|
+
client.request("interactions.respond", {
|
|
7692
|
+
requestId: request.id,
|
|
7693
|
+
response
|
|
7694
|
+
}).catch(() => void 0);
|
|
7695
|
+
},
|
|
7696
|
+
cancel: () => void 0
|
|
7697
|
+
});
|
|
7698
|
+
};
|
|
7699
|
+
const unsubscribe = client.onSignal((signal) => {
|
|
7700
|
+
switch (signal.kind) {
|
|
7701
|
+
case "approval:request":
|
|
7702
|
+
surfaceApproval(signal.request);
|
|
7703
|
+
return;
|
|
7704
|
+
case "settings:changed":
|
|
7705
|
+
callbacks.onSettingsChanged?.(signal.settings);
|
|
7706
|
+
return;
|
|
7707
|
+
case "sessions:changed":
|
|
7708
|
+
callbacks.onSessionsChanged?.(signal.op, signal.sessionId);
|
|
7709
|
+
return;
|
|
7710
|
+
case "presence:changed":
|
|
7711
|
+
callbacks.onPresenceChanged?.(signal.sessions);
|
|
7712
|
+
return;
|
|
7713
|
+
case "hud:state":
|
|
7714
|
+
callbacks.onHudState?.({
|
|
7715
|
+
statuses: signal.statuses,
|
|
7716
|
+
banners: signal.banners
|
|
7717
|
+
});
|
|
7718
|
+
return;
|
|
7719
|
+
case "hud:notify":
|
|
7720
|
+
callbacks.onHudNotify?.(signal);
|
|
7721
|
+
return;
|
|
7722
|
+
case "extension:signal":
|
|
7723
|
+
callbacks.onExtensionSignal?.({
|
|
7724
|
+
extensionId: signal.extensionId,
|
|
7725
|
+
...signal.sessionId !== void 0 ? { sessionId: signal.sessionId } : {},
|
|
7726
|
+
payload: signal.payload,
|
|
7727
|
+
seq: signal.seq,
|
|
7728
|
+
...signal.nonce !== void 0 ? { nonce: signal.nonce } : {}
|
|
7729
|
+
});
|
|
7730
|
+
return;
|
|
7731
|
+
case "approval:resolved":
|
|
7732
|
+
if (signal.sessionId === sessionId) {
|
|
7733
|
+
resolvedHitl.add(signal.requestId);
|
|
7734
|
+
if (surfacedHitl.has(signal.requestId)) callbacks.onApprovalResolved?.(signal.requestId);
|
|
7735
|
+
}
|
|
7736
|
+
return;
|
|
7737
|
+
case "interaction:request":
|
|
7738
|
+
if (signal.sessionId === sessionId) surfaceInteraction(signal.request);
|
|
7739
|
+
return;
|
|
7740
|
+
case "interaction:resolved":
|
|
7741
|
+
if (signal.sessionId === sessionId) {
|
|
7742
|
+
resolvedHitl.add(signal.requestId);
|
|
7743
|
+
if (surfacedHitl.has(signal.requestId)) callbacks.onInteractionResolved?.(signal.requestId);
|
|
7744
|
+
}
|
|
7745
|
+
return;
|
|
7746
|
+
default: break;
|
|
7747
|
+
}
|
|
7748
|
+
if (!("sessionId" in signal) || signal.sessionId !== sessionId) return;
|
|
7749
|
+
switch (signal.kind) {
|
|
7750
|
+
case "stream:retry":
|
|
7751
|
+
callbacks.onRetry?.({
|
|
7752
|
+
attempt: signal.attempt,
|
|
7753
|
+
max: signal.max,
|
|
7754
|
+
source: signal.source
|
|
7755
|
+
});
|
|
7756
|
+
break;
|
|
7757
|
+
case "turn:end":
|
|
7758
|
+
callbacks.onTurnEnd?.({
|
|
7759
|
+
turnId: signal.turnId,
|
|
7760
|
+
...signal.usage ? { usage: signal.usage } : {},
|
|
7761
|
+
...signal.childId ? { childId: signal.childId } : {}
|
|
7762
|
+
});
|
|
7763
|
+
break;
|
|
7764
|
+
case "run:start":
|
|
7765
|
+
running = true;
|
|
7766
|
+
sawRunStart = true;
|
|
7767
|
+
callbacks.onRunState(true);
|
|
7768
|
+
break;
|
|
7769
|
+
case "run:end":
|
|
7770
|
+
running = false;
|
|
7771
|
+
callbacks.onRunState(false, {
|
|
7772
|
+
ok: signal.ok,
|
|
7773
|
+
...signal.error ? { error: signal.error } : {}
|
|
7774
|
+
});
|
|
7775
|
+
break;
|
|
7776
|
+
case "event":
|
|
7777
|
+
if (transcriptReady) callbacks.onStreamEvent(signal.event);
|
|
7778
|
+
else bufferedEvents.push(signal.event);
|
|
7779
|
+
break;
|
|
7780
|
+
case "compacting:progress":
|
|
7781
|
+
callbacks.onCompactionProgress?.(signal.estOutputTokens);
|
|
7782
|
+
break;
|
|
7783
|
+
case "prompt-turns":
|
|
7784
|
+
callbacks.onPromptTurns(signal.refs);
|
|
7785
|
+
break;
|
|
7786
|
+
case "stream:retry:cleared":
|
|
7787
|
+
callbacks.onRetryCleared(signal.source);
|
|
7788
|
+
break;
|
|
7789
|
+
case "compacting:start":
|
|
7790
|
+
callbacks.onCompacting?.(true);
|
|
7791
|
+
break;
|
|
7792
|
+
case "compacting:end":
|
|
7793
|
+
callbacks.onCompacting?.(false, {
|
|
7794
|
+
ok: signal.ok,
|
|
7795
|
+
...signal.effectiveTokens !== void 0 ? { effectiveTokens: signal.effectiveTokens } : {}
|
|
7796
|
+
});
|
|
7797
|
+
break;
|
|
7798
|
+
case "model:fallback":
|
|
7799
|
+
callbacks.onModelFallback?.(signal.from, signal.to);
|
|
7800
|
+
break;
|
|
7801
|
+
case "draft:changed":
|
|
7802
|
+
callbacks.onDraftChanged?.(signal.text, signal.shell === true);
|
|
7803
|
+
break;
|
|
7804
|
+
case "draft:attachments":
|
|
7805
|
+
callbacks.onDraftAttachmentsChanged?.(signal.attachments);
|
|
7806
|
+
break;
|
|
7807
|
+
case "search:changed":
|
|
7808
|
+
callbacks.onSearchChanged?.({
|
|
7809
|
+
active: signal.active,
|
|
7810
|
+
query: signal.query
|
|
7811
|
+
});
|
|
7812
|
+
break;
|
|
7813
|
+
case "suggest:changed":
|
|
7814
|
+
callbacks.onSuggestChanged?.({
|
|
7815
|
+
seq: signal.seq,
|
|
7816
|
+
basis: signal.basis,
|
|
7817
|
+
caret: signal.caret,
|
|
7818
|
+
suggestion: signal.suggestion,
|
|
7819
|
+
...signal.candidates ? { candidates: signal.candidates } : {}
|
|
7820
|
+
});
|
|
7821
|
+
break;
|
|
7822
|
+
case "skills:snapshot":
|
|
7823
|
+
callbacks.onSkillsChanged?.(signal.active);
|
|
7824
|
+
break;
|
|
7825
|
+
case "mcp:auth":
|
|
7826
|
+
callbacks.onMcpAuth?.({
|
|
7827
|
+
name: signal.name,
|
|
7828
|
+
reason: signal.reason
|
|
7829
|
+
});
|
|
7830
|
+
break;
|
|
7831
|
+
case "queue:changed":
|
|
7832
|
+
callbacks.onQueueChanged?.(signal.items);
|
|
7833
|
+
break;
|
|
7834
|
+
case "work:snapshot":
|
|
7835
|
+
callbacks.onWorkChanged?.(signal.items);
|
|
7836
|
+
break;
|
|
7837
|
+
case "todos:snapshot":
|
|
7838
|
+
callbacks.onTodosChanged?.(signal.snapshot);
|
|
7839
|
+
break;
|
|
7840
|
+
case "cache:break":
|
|
7841
|
+
callbacks.onCacheBreak(signal.info);
|
|
7842
|
+
break;
|
|
7843
|
+
case "system-md":
|
|
7844
|
+
callbacks.onSystemMd(signal.file, signal.shadowed);
|
|
7845
|
+
break;
|
|
7846
|
+
default: break;
|
|
7847
|
+
}
|
|
7848
|
+
});
|
|
7849
|
+
try {
|
|
7850
|
+
const initialEvents = await client.request("sessions.loadEvents", { sessionId });
|
|
7851
|
+
callbacks.onInitialTranscript(initialEvents);
|
|
7852
|
+
transcriptReady = true;
|
|
7853
|
+
for (const event of bufferedEvents.splice(0)) callbacks.onStreamEvent(event);
|
|
7854
|
+
if (!sawRunStart) {
|
|
7855
|
+
running = await client.request("chat.isRunning", { sessionId });
|
|
7856
|
+
if (running) callbacks.onRunState(true);
|
|
7857
|
+
}
|
|
7858
|
+
for (const request of await client.request("approvals.listPending", {})) surfaceApproval(request);
|
|
7859
|
+
await client.request("interactions.rehydrate", { sessionId });
|
|
7860
|
+
for (const pending of await client.request("interactions.list", { sessionId })) surfaceInteraction(pending.request);
|
|
7861
|
+
client.notify("presence.watch", { sessionId });
|
|
7862
|
+
const [presence, queued, draft, systemMd, todos] = await Promise.all([
|
|
7863
|
+
optional(() => client.request("presence.list", {})),
|
|
7864
|
+
optional(() => client.request("queue.list", { sessionId })),
|
|
7865
|
+
optional(() => client.request("drafts.get", { sessionId })),
|
|
7866
|
+
optional(() => client.request("sessions.systemMd", { sessionId })),
|
|
7867
|
+
optional(() => client.request("todos.get", { sessionId }))
|
|
7868
|
+
]);
|
|
7869
|
+
if (presence) callbacks.onPresenceChanged?.(presence);
|
|
7870
|
+
if (queued && queued.length > 0) callbacks.onQueueChanged?.(queued);
|
|
7871
|
+
if (draft && (draft.text.length > 0 || draft.shell)) callbacks.onDraftChanged?.(draft.text, draft.shell === true);
|
|
7872
|
+
if (draft?.attachments?.length) callbacks.onDraftAttachmentsChanged?.(draft.attachments);
|
|
7873
|
+
callbacks.onSearchChanged?.(draft?.search ?? {
|
|
7874
|
+
active: false,
|
|
7875
|
+
query: ""
|
|
7876
|
+
});
|
|
7877
|
+
callbacks.onSystemMd(systemMd?.file ?? null, systemMd?.shadowed ?? []);
|
|
7878
|
+
if (todos) callbacks.onTodosChanged?.(todos);
|
|
7879
|
+
} catch (err) {
|
|
7880
|
+
unsubscribe();
|
|
7881
|
+
client.close();
|
|
7882
|
+
throw err;
|
|
7883
|
+
}
|
|
7884
|
+
return {
|
|
7885
|
+
sessionId,
|
|
7886
|
+
get isRunning() {
|
|
7887
|
+
return running;
|
|
7888
|
+
},
|
|
7889
|
+
async send(prompt, sendOptions) {
|
|
7890
|
+
if (sendOptions?.attachments?.length) {
|
|
7891
|
+
await client.request("chat.send", {
|
|
7892
|
+
sessionId,
|
|
7893
|
+
prompt,
|
|
7894
|
+
attachments: sendOptions.attachments,
|
|
7895
|
+
...sendOptions.activatedSkills?.length ? { activatedSkills: sendOptions.activatedSkills } : {}
|
|
7896
|
+
});
|
|
7897
|
+
return "send";
|
|
7898
|
+
}
|
|
7899
|
+
const { delivered } = await client.request("chat.pushQueued", {
|
|
7900
|
+
sessionId,
|
|
7901
|
+
message: prompt,
|
|
7902
|
+
...sendOptions?.activatedSkills?.length ? { activatedSkills: sendOptions.activatedSkills } : {}
|
|
7903
|
+
});
|
|
7904
|
+
return delivered;
|
|
7905
|
+
},
|
|
7906
|
+
interrupt() {
|
|
7907
|
+
client.request("chat.interrupt", { sessionId }).catch(() => void 0);
|
|
7908
|
+
},
|
|
7909
|
+
publishSettings(patch) {
|
|
7910
|
+
client.request("settings.set", { patch }).catch(() => void 0);
|
|
7911
|
+
},
|
|
7912
|
+
touchSession() {
|
|
7913
|
+
client.notify("sessions.touch", { sessionId });
|
|
7914
|
+
},
|
|
7915
|
+
publishDraftAttachments(attachments) {
|
|
7916
|
+
client.notify("drafts.setAttachments", {
|
|
7917
|
+
sessionId,
|
|
7918
|
+
attachments
|
|
7919
|
+
});
|
|
7920
|
+
},
|
|
7921
|
+
publishSearch(active, query) {
|
|
7922
|
+
client.notify("search.set", {
|
|
7923
|
+
sessionId,
|
|
7924
|
+
active,
|
|
7925
|
+
query
|
|
7926
|
+
});
|
|
7927
|
+
},
|
|
7928
|
+
emitExtension(envelope) {
|
|
7929
|
+
client.notify("extension.emit", envelope);
|
|
7930
|
+
},
|
|
7931
|
+
async resolveSuggestion(text, caret) {
|
|
7932
|
+
suggestSeq += 1;
|
|
7933
|
+
return optional(() => client.request("suggest.resolve", {
|
|
7934
|
+
sessionId,
|
|
7935
|
+
text,
|
|
7936
|
+
caret,
|
|
7937
|
+
seq: suggestSeq
|
|
7938
|
+
}));
|
|
7939
|
+
},
|
|
7940
|
+
publishDraft(text, shell) {
|
|
7941
|
+
client.notify("drafts.set", {
|
|
7942
|
+
sessionId,
|
|
7943
|
+
text,
|
|
7944
|
+
...shell ? { shell: true } : {}
|
|
7945
|
+
});
|
|
7946
|
+
},
|
|
7947
|
+
async queueAdd(text, options) {
|
|
7948
|
+
const result = await optional(() => client.request("queue.add", {
|
|
7949
|
+
sessionId,
|
|
7950
|
+
text,
|
|
7951
|
+
...options?.activatedSkills?.length ? { activatedSkills: options.activatedSkills } : {},
|
|
7952
|
+
...options?.attachments?.length ? { attachments: options.attachments } : {}
|
|
7953
|
+
}));
|
|
7954
|
+
if (result) return result.delivered;
|
|
7955
|
+
const { delivered } = await client.request("chat.pushQueued", {
|
|
7956
|
+
sessionId,
|
|
7957
|
+
message: text,
|
|
7958
|
+
...options?.activatedSkills?.length ? { activatedSkills: options.activatedSkills } : {}
|
|
7959
|
+
});
|
|
7960
|
+
return delivered === "steer" ? "queued" : "sent";
|
|
7961
|
+
},
|
|
7962
|
+
async queueRemove(id) {
|
|
7963
|
+
return await optional(() => client.request("queue.remove", {
|
|
7964
|
+
sessionId,
|
|
7965
|
+
id
|
|
7966
|
+
})) ?? false;
|
|
7967
|
+
},
|
|
7968
|
+
async setTodos(items) {
|
|
7969
|
+
return await optional(() => client.request("todos.set", {
|
|
7970
|
+
sessionId,
|
|
7971
|
+
items
|
|
7972
|
+
})) ?? null;
|
|
7973
|
+
},
|
|
7974
|
+
async queuePushNow(id) {
|
|
7975
|
+
return (await optional(() => client.request("queue.pushNow", {
|
|
7976
|
+
sessionId,
|
|
7977
|
+
id
|
|
7978
|
+
})))?.delivered ?? "missing";
|
|
7979
|
+
},
|
|
7980
|
+
async fetchTranscript() {
|
|
7981
|
+
return optional(() => client.request("sessions.loadEvents", { sessionId }));
|
|
7982
|
+
},
|
|
7983
|
+
async applyModel(options) {
|
|
7984
|
+
await optional(() => client.request("sessions.setModel", {
|
|
7985
|
+
sessionId,
|
|
7986
|
+
providerKey: options.providerKey,
|
|
7987
|
+
modelId: options.modelId
|
|
7988
|
+
}));
|
|
7989
|
+
if (options.effort !== void 0) await optional(() => client.request("sessions.setEffort", {
|
|
7990
|
+
sessionId,
|
|
7991
|
+
effort: options.effort
|
|
7992
|
+
}));
|
|
7993
|
+
if (options.modelOptions !== void 0) await optional(() => client.request("sessions.setModelOptions", {
|
|
7994
|
+
sessionId,
|
|
7995
|
+
options: options.modelOptions
|
|
7996
|
+
}));
|
|
7997
|
+
},
|
|
7998
|
+
async setAgentId(agentId) {
|
|
7999
|
+
await optional(() => client.request("sessions.setAgentId", {
|
|
8000
|
+
sessionId,
|
|
8001
|
+
agentId
|
|
8002
|
+
}));
|
|
8003
|
+
},
|
|
8004
|
+
async fetchContextBreakdown() {
|
|
8005
|
+
return optional(() => client.request("sessions.contextBreakdown", { sessionId }));
|
|
8006
|
+
},
|
|
8007
|
+
async mainSession() {
|
|
8008
|
+
return optional(() => client.request("orchestration.main", {}));
|
|
8009
|
+
},
|
|
8010
|
+
async searchTranscript(query, limit) {
|
|
8011
|
+
return optional(() => client.request("history.searchSession", {
|
|
8012
|
+
sessionId,
|
|
8013
|
+
query,
|
|
8014
|
+
...limit !== void 0 ? { limit } : {}
|
|
8015
|
+
}));
|
|
8016
|
+
},
|
|
8017
|
+
async searchHistory(query, sessionIds, excerptLimit) {
|
|
8018
|
+
return optional(() => client.request("history.searchAll", {
|
|
8019
|
+
query,
|
|
8020
|
+
sessionIds,
|
|
8021
|
+
...excerptLimit !== void 0 ? { excerptLimit } : {}
|
|
8022
|
+
}));
|
|
8023
|
+
},
|
|
8024
|
+
async rebuildAgent() {
|
|
8025
|
+
await optional(() => client.request("chat.rebuildAgent", { sessionId }));
|
|
8026
|
+
},
|
|
8027
|
+
async fetchPresence() {
|
|
8028
|
+
return optional(() => client.request("presence.list", {}));
|
|
8029
|
+
},
|
|
8030
|
+
async cancelWork(id, reason) {
|
|
8031
|
+
return await optional(() => client.request("chat.cancelWork", {
|
|
8032
|
+
sessionId,
|
|
8033
|
+
id,
|
|
8034
|
+
...reason ? { reason } : {}
|
|
8035
|
+
})) ?? false;
|
|
8036
|
+
},
|
|
8037
|
+
dispose() {
|
|
8038
|
+
disposed = true;
|
|
8039
|
+
unsubscribe();
|
|
8040
|
+
client.close();
|
|
8041
|
+
}
|
|
8042
|
+
};
|
|
8043
|
+
}
|
|
8044
|
+
//#endregion
|
|
7567
8045
|
//#region src/chat/external-editor.ts
|
|
7568
8046
|
/**
|
|
7569
8047
|
* External-editor handoff — open a piece of text in the user's `$EDITOR`,
|
|
@@ -8256,16 +8734,69 @@ function CompletionPopup({ state, visibleRows = 6 }) {
|
|
|
8256
8734
|
});
|
|
8257
8735
|
}
|
|
8258
8736
|
//#endregion
|
|
8737
|
+
//#region src/tui/spinner.ts
|
|
8738
|
+
/**
|
|
8739
|
+
* Shared braille spinner language — one frame set + cadence for every
|
|
8740
|
+
* inline working glyph in the TUI (extension HUD loader, session-list
|
|
8741
|
+
* presence spinner). Kept out of any component module so consumers on
|
|
8742
|
+
* different screens don't drag each other's imports in.
|
|
8743
|
+
*/
|
|
8744
|
+
const SPINNER_FRAMES = [
|
|
8745
|
+
"⠋",
|
|
8746
|
+
"⠙",
|
|
8747
|
+
"⠹",
|
|
8748
|
+
"⠸",
|
|
8749
|
+
"⠼",
|
|
8750
|
+
"⠴",
|
|
8751
|
+
"⠦",
|
|
8752
|
+
"⠧",
|
|
8753
|
+
"⠇",
|
|
8754
|
+
"⠏"
|
|
8755
|
+
];
|
|
8756
|
+
/**
|
|
8757
|
+
* Frame index ticking at {@link SPINNER_INTERVAL_MS} — the interval only
|
|
8758
|
+
* runs while `active`; an idle consumer re-renders zero times. Returns
|
|
8759
|
+
* the raw index (not the glyph) so callers can also derive slower
|
|
8760
|
+
* secondary rhythms (e.g. a pulse) from the same clock.
|
|
8761
|
+
*
|
|
8762
|
+
* The index wraps at `SPINNER_FRAMES.length` (even), so `frame % 2`
|
|
8763
|
+
* parity stays continuous across the wrap.
|
|
8764
|
+
*/
|
|
8765
|
+
function useSpinnerFrame(active) {
|
|
8766
|
+
const [frame, setFrame] = useState(0);
|
|
8767
|
+
useEffect(() => {
|
|
8768
|
+
if (!active) return;
|
|
8769
|
+
const timer = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 120);
|
|
8770
|
+
return () => clearInterval(timer);
|
|
8771
|
+
}, [active]);
|
|
8772
|
+
return frame;
|
|
8773
|
+
}
|
|
8774
|
+
//#endregion
|
|
8259
8775
|
//#region src/tui/tui-hud.ts
|
|
8260
|
-
let holder$
|
|
8776
|
+
let holder$4 = null;
|
|
8261
8777
|
/** Initialize (or return) the process-wide HUD. Idempotent. */
|
|
8262
8778
|
function initTuiHud() {
|
|
8263
|
-
holder$
|
|
8264
|
-
return holder$
|
|
8779
|
+
holder$4 ??= createExtensionHud();
|
|
8780
|
+
return holder$4;
|
|
8265
8781
|
}
|
|
8266
8782
|
/** The HUD, or null when `runTui` hasn't wired it (bare embedders). */
|
|
8267
8783
|
function getTuiHud() {
|
|
8268
|
-
return holder$
|
|
8784
|
+
return holder$4;
|
|
8785
|
+
}
|
|
8786
|
+
let remoteState = null;
|
|
8787
|
+
const remoteListeners = /* @__PURE__ */ new Set();
|
|
8788
|
+
function setRemoteHudState(state) {
|
|
8789
|
+
remoteState = state;
|
|
8790
|
+
for (const listener of remoteListeners) listener();
|
|
8791
|
+
}
|
|
8792
|
+
function getRemoteHudState() {
|
|
8793
|
+
return remoteState;
|
|
8794
|
+
}
|
|
8795
|
+
function subscribeRemoteHudState(listener) {
|
|
8796
|
+
remoteListeners.add(listener);
|
|
8797
|
+
return () => {
|
|
8798
|
+
remoteListeners.delete(listener);
|
|
8799
|
+
};
|
|
8269
8800
|
}
|
|
8270
8801
|
//#endregion
|
|
8271
8802
|
//#region src/tui/extension-hud.tsx
|
|
@@ -8273,10 +8804,27 @@ const EMPTY_STATE = {
|
|
|
8273
8804
|
statuses: {},
|
|
8274
8805
|
banners: {}
|
|
8275
8806
|
};
|
|
8276
|
-
/**
|
|
8807
|
+
/**
|
|
8808
|
+
* Subscribe to the HUD (inert empty state when unwired), merged with the
|
|
8809
|
+
* remote overlay forwarded from the daemon (whose extension hooks run
|
|
8810
|
+
* out-of-process). Local entries win per extension name — a local write
|
|
8811
|
+
* is this window's own action.
|
|
8812
|
+
*/
|
|
8277
8813
|
function useHudState() {
|
|
8278
8814
|
const hud = getTuiHud();
|
|
8279
|
-
|
|
8815
|
+
const local = useSyncExternalStore((listener) => hud ? hud.subscribe(listener) : () => {}, () => hud ? hud.getState() : EMPTY_STATE);
|
|
8816
|
+
const remote = useSyncExternalStore(subscribeRemoteHudState, getRemoteHudState);
|
|
8817
|
+
if (!remote) return local;
|
|
8818
|
+
return {
|
|
8819
|
+
statuses: {
|
|
8820
|
+
...remote.statuses,
|
|
8821
|
+
...local.statuses
|
|
8822
|
+
},
|
|
8823
|
+
banners: {
|
|
8824
|
+
...remote.banners,
|
|
8825
|
+
...local.banners
|
|
8826
|
+
}
|
|
8827
|
+
};
|
|
8280
8828
|
}
|
|
8281
8829
|
/**
|
|
8282
8830
|
* Live enabled-set filter — a Settings toggle-off hides the extension's
|
|
@@ -8291,34 +8839,11 @@ function useVisibleEntries(record) {
|
|
|
8291
8839
|
const entries = Object.entries(record);
|
|
8292
8840
|
return allow === void 0 ? entries : entries.filter(([name]) => allow.includes(name));
|
|
8293
8841
|
}
|
|
8294
|
-
const LOADER_FRAMES = [
|
|
8295
|
-
"⠋",
|
|
8296
|
-
"⠙",
|
|
8297
|
-
"⠹",
|
|
8298
|
-
"⠸",
|
|
8299
|
-
"⠼",
|
|
8300
|
-
"⠴",
|
|
8301
|
-
"⠦",
|
|
8302
|
-
"⠧",
|
|
8303
|
-
"⠇",
|
|
8304
|
-
"⠏"
|
|
8305
|
-
];
|
|
8306
|
-
const LOADER_INTERVAL_MS = 120;
|
|
8307
|
-
/** Animated loader frame — the interval only runs while a loader is visible. */
|
|
8308
|
-
function useLoaderFrame(active) {
|
|
8309
|
-
const [frame, setFrame] = useState(0);
|
|
8310
|
-
useEffect(() => {
|
|
8311
|
-
if (!active) return;
|
|
8312
|
-
const timer = setInterval(() => setFrame((f) => (f + 1) % LOADER_FRAMES.length), LOADER_INTERVAL_MS);
|
|
8313
|
-
return () => clearInterval(timer);
|
|
8314
|
-
}, [active]);
|
|
8315
|
-
return LOADER_FRAMES[frame % LOADER_FRAMES.length];
|
|
8316
|
-
}
|
|
8317
8842
|
function ExtensionStatusLine() {
|
|
8318
8843
|
const COLOR = useColors();
|
|
8319
8844
|
const { statuses } = useHudState();
|
|
8320
8845
|
const entries = useVisibleEntries(statuses);
|
|
8321
|
-
const glyph =
|
|
8846
|
+
const glyph = SPINNER_FRAMES[useSpinnerFrame(entries.some(([, status]) => status.loader)) % SPINNER_FRAMES.length];
|
|
8322
8847
|
if (entries.length === 0) return null;
|
|
8323
8848
|
return /* @__PURE__ */ jsx$1("text", {
|
|
8324
8849
|
fg: COLOR.dim,
|
|
@@ -11496,7 +12021,7 @@ const PAGE_JUMP = 6;
|
|
|
11496
12021
|
function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreate, onFocusChange, contentMatches = /* @__PURE__ */ new Map(), contentSearching = false, contentSearchProgress = {
|
|
11497
12022
|
done: 0,
|
|
11498
12023
|
total: 0
|
|
11499
|
-
}, onSearchQueryChange, showAllProjects = false, onToggleScope, currentProjectRoot }) {
|
|
12024
|
+
}, onSearchQueryChange, showAllProjects = false, scopeLabel, presence, onToggleScope, currentProjectRoot, mainSessionId = null }) {
|
|
11500
12025
|
const focused = useModalAwareFocus();
|
|
11501
12026
|
const COLOR = useColors();
|
|
11502
12027
|
const inputRef = useRef(null);
|
|
@@ -11519,26 +12044,50 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
|
|
|
11519
12044
|
query,
|
|
11520
12045
|
contentMatches
|
|
11521
12046
|
]);
|
|
11522
|
-
const rows = useMemo(() =>
|
|
11523
|
-
|
|
11524
|
-
|
|
11525
|
-
|
|
11526
|
-
|
|
11527
|
-
|
|
11528
|
-
|
|
11529
|
-
|
|
11530
|
-
|
|
11531
|
-
|
|
12047
|
+
const rows = useMemo(() => {
|
|
12048
|
+
const isMain = (meta) => meta.isMain === true || meta.id === mainSessionId;
|
|
12049
|
+
const sessionRows = filteredSessions.map((meta) => ({
|
|
12050
|
+
kind: "session",
|
|
12051
|
+
rowId: meta.id,
|
|
12052
|
+
meta,
|
|
12053
|
+
...isMain(meta) ? { isMain: true } : {},
|
|
12054
|
+
...query.trim() && contentMatches.get(meta.id) ? { contentMatch: contentMatches.get(meta.id) } : {}
|
|
12055
|
+
}));
|
|
12056
|
+
const mainIndex = sessionRows.findIndex((row) => row.isMain);
|
|
12057
|
+
return [
|
|
12058
|
+
...mainIndex >= 0 ? sessionRows.splice(mainIndex, 1) : [],
|
|
12059
|
+
{
|
|
12060
|
+
kind: "new",
|
|
12061
|
+
rowId: NEW_SESSION_ROW_ID,
|
|
12062
|
+
meta: null
|
|
12063
|
+
},
|
|
12064
|
+
...sessionRows
|
|
12065
|
+
];
|
|
12066
|
+
}, [
|
|
12067
|
+
filteredSessions,
|
|
12068
|
+
contentMatches,
|
|
12069
|
+
mainSessionId
|
|
12070
|
+
]);
|
|
12071
|
+
const spinnerFrame = useSpinnerFrame(useMemo(() => {
|
|
12072
|
+
if (!presence) return false;
|
|
12073
|
+
return rows.some((row) => {
|
|
12074
|
+
if (row.kind !== "session") return false;
|
|
12075
|
+
const info = presence.get(row.rowId);
|
|
12076
|
+
return info !== void 0 && (info.attention || info.running);
|
|
12077
|
+
});
|
|
12078
|
+
}, [rows, presence]));
|
|
11532
12079
|
useEffect(() => {
|
|
11533
12080
|
if (focused) inputRef.current?.focus();
|
|
11534
12081
|
}, [focused]);
|
|
11535
12082
|
const cursorIndex = useMemo(() => {
|
|
11536
|
-
|
|
12083
|
+
const newIdx = Math.max(0, rows.findIndex((r) => r.kind === "new"));
|
|
12084
|
+
if (focusedSessionId === "__new__") return newIdx;
|
|
11537
12085
|
if (focusedSessionId) {
|
|
11538
12086
|
const idx = rows.findIndex((r) => r.kind === "session" && r.rowId === focusedSessionId);
|
|
11539
12087
|
if (idx !== -1) return idx;
|
|
11540
12088
|
}
|
|
11541
|
-
|
|
12089
|
+
const firstOrdinary = rows.findIndex((r) => r.kind === "session" && !r.isMain);
|
|
12090
|
+
return firstOrdinary !== -1 ? firstOrdinary : newIdx;
|
|
11542
12091
|
}, [rows, focusedSessionId]);
|
|
11543
12092
|
const lastScrolledCursorRef = useRef(-1);
|
|
11544
12093
|
useEffect(() => {
|
|
@@ -11645,7 +12194,7 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
|
|
|
11645
12194
|
const OVERLAY_RESERVED = 6;
|
|
11646
12195
|
const TITLE_LEN = 8;
|
|
11647
12196
|
const SEP_LEN = 3;
|
|
11648
|
-
const allProjectsLen = showAllProjects ?
|
|
12197
|
+
const allProjectsLen = showAllProjects ? (scopeLabel ?? "all projects").length + SEP_LEN : 0;
|
|
11649
12198
|
const countLen = countSegs.reduce((sum, s) => sum + s.text.length, 0);
|
|
11650
12199
|
const cwdBudget = Math.max(0, termWidth - 4 - TITLE_LEN - OVERLAY_RESERVED) - countLen - SEP_LEN - allProjectsLen;
|
|
11651
12200
|
if (cwdBudget < 6) return countSegs;
|
|
@@ -11657,7 +12206,7 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
|
|
|
11657
12206
|
text: " · ",
|
|
11658
12207
|
color: COLOR.mute
|
|
11659
12208
|
}, {
|
|
11660
|
-
text: "all projects",
|
|
12209
|
+
text: scopeLabel ?? "all projects",
|
|
11661
12210
|
color: COLOR.accent
|
|
11662
12211
|
});
|
|
11663
12212
|
segs.push({
|
|
@@ -11722,6 +12271,8 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
|
|
|
11722
12271
|
isCurrent: row.kind === "session" && row.rowId === currentId,
|
|
11723
12272
|
showProject: showAllProjects,
|
|
11724
12273
|
currentProjectRoot,
|
|
12274
|
+
presence: row.kind === "session" ? presence?.get(row.rowId) : void 0,
|
|
12275
|
+
spinnerFrame,
|
|
11725
12276
|
onHover: focused ? () => moveCursor(idx) : void 0,
|
|
11726
12277
|
onClick: focused ? () => {
|
|
11727
12278
|
moveCursor(idx);
|
|
@@ -11769,7 +12320,7 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
|
|
|
11769
12320
|
* exactly the width of the focus + current markers above — so the first
|
|
11770
12321
|
* stat (the turn count) sits flush under the title's first letter.
|
|
11771
12322
|
*/
|
|
11772
|
-
function SessionRow({ row, anchorId, focused, isCurrent, separator = false, showProject = false, currentProjectRoot, onClick, onHover }) {
|
|
12323
|
+
function SessionRow({ row, anchorId, focused, isCurrent, separator = false, showProject = false, currentProjectRoot, presence, spinnerFrame = 0, onClick, onHover }) {
|
|
11773
12324
|
const COLOR = useColors();
|
|
11774
12325
|
const SURFACE = useSurfaces();
|
|
11775
12326
|
const STATS_INDENT = " ";
|
|
@@ -11843,10 +12394,21 @@ function SessionRow({ row, anchorId, focused, isCurrent, separator = false, show
|
|
|
11843
12394
|
fg: currentColor,
|
|
11844
12395
|
children: currentMark
|
|
11845
12396
|
}),
|
|
12397
|
+
row.isMain && /* @__PURE__ */ jsx$1("span", {
|
|
12398
|
+
fg: COLOR.brand,
|
|
12399
|
+
children: "◆ "
|
|
12400
|
+
}),
|
|
11846
12401
|
/* @__PURE__ */ jsx$1("span", {
|
|
11847
|
-
fg: titleColor,
|
|
12402
|
+
fg: row.isMain ? COLOR.brand : titleColor,
|
|
11848
12403
|
children: meta.title
|
|
11849
|
-
})
|
|
12404
|
+
}),
|
|
12405
|
+
presence && (() => {
|
|
12406
|
+
const glyph = presence.attention ? spinnerFrame % 2 === 0 ? "◉" : "◎" : presence.running ? SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length] : presence.windows > 0 ? "○" : " ";
|
|
12407
|
+
return /* @__PURE__ */ jsx$1("span", {
|
|
12408
|
+
fg: presence.attention ? COLOR.warn : presence.running ? COLOR.brand : COLOR.accent,
|
|
12409
|
+
children: ` ${glyph}`
|
|
12410
|
+
});
|
|
12411
|
+
})()
|
|
11850
12412
|
]
|
|
11851
12413
|
}),
|
|
11852
12414
|
/* @__PURE__ */ jsxs("text", {
|
|
@@ -12024,7 +12586,7 @@ const EMPTY_QUEUED_MESSAGES = [];
|
|
|
12024
12586
|
function shouldRenderTranscriptSearch(searchMode, query) {
|
|
12025
12587
|
return searchMode && query.trim().length > 0;
|
|
12026
12588
|
}
|
|
12027
|
-
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, onSelectTurn, onOpenTurn, transcriptSearchMode = false, transcriptSearchQuery = "", transcriptSearchEvents = [], transcriptSearchSearching = false, onTranscriptSearchModeChange, onTranscriptSearchQueryChange, onTranscriptSearchNavigate, onTranscriptSearchOpen, onLoadEarlierTranscript, promptTriggerHints, attachImageBinding, editorBinding, sessionDetailsBinding }) {
|
|
12589
|
+
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, onSelectTurn, onOpenTurn, onOpenSessionEvent, transcriptSearchMode = false, transcriptSearchQuery = "", transcriptSearchEvents = [], transcriptSearchSearching = false, onTranscriptSearchModeChange, onTranscriptSearchQueryChange, onTranscriptSearchNavigate, onTranscriptSearchOpen, onLoadEarlierTranscript, promptTriggerHints, attachImageBinding, editorBinding, sessionDetailsBinding, onDraftChange, remoteDraft, onDraftAttachmentsChange, remoteDraftAttachments, remoteSearchQuery, remoteSuggest, resolveGhostRemote }) {
|
|
12028
12590
|
const COLOR = useColors();
|
|
12029
12591
|
const SURFACE = useSurfaces();
|
|
12030
12592
|
const titleText = session?.title ?? "New session";
|
|
@@ -12119,6 +12681,7 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
|
|
|
12119
12681
|
selectedTurnId: selectedTurnId ?? null,
|
|
12120
12682
|
onSelectTurn,
|
|
12121
12683
|
onOpenTurn,
|
|
12684
|
+
onOpenSessionEvent,
|
|
12122
12685
|
searchQuery: transcriptSearchActive ? transcriptSearchQuery : void 0,
|
|
12123
12686
|
searchPending: transcriptSearchSearching,
|
|
12124
12687
|
busy: !transcriptSearchActive && busy && !pending && !pendingInteraction,
|
|
@@ -12195,7 +12758,14 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
|
|
|
12195
12758
|
onEnterQueueFromEmpty: onEnterQueueFromEmptyPrompt,
|
|
12196
12759
|
attachImageBinding,
|
|
12197
12760
|
editorBinding,
|
|
12198
|
-
suspended: approvalPending != null || pendingInteraction != null
|
|
12761
|
+
suspended: approvalPending != null || pendingInteraction != null,
|
|
12762
|
+
onDraftChange,
|
|
12763
|
+
remoteDraft,
|
|
12764
|
+
onDraftAttachmentsChange,
|
|
12765
|
+
remoteDraftAttachments,
|
|
12766
|
+
remoteSearchQuery,
|
|
12767
|
+
remoteSuggest,
|
|
12768
|
+
resolveGhostRemote
|
|
12199
12769
|
})
|
|
12200
12770
|
]
|
|
12201
12771
|
})
|
|
@@ -12542,7 +13112,7 @@ const INLINE_GHOST_DEBOUNCE_MS = 120;
|
|
|
12542
13112
|
function isTranscriptSearchExitKey(event, cursorOffset) {
|
|
12543
13113
|
return event.name === "escape" || (event.name === "backspace" || event.name === "delete") && !event.ctrl && !event.meta && !event.shift && cursorOffset === 0;
|
|
12544
13114
|
}
|
|
12545
|
-
function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, searchMode = false, onSearchModeChange, onSearchQueryChange, onSearchNavigate, onSearchOpen, selectMode = null, triggerHints, busy = false, onEnterQueueFromEmpty, attachImageBinding, editorBinding, clipboardReader = readClipboardContent, suspended = false }) {
|
|
13115
|
+
function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, searchMode = false, onSearchModeChange, onSearchQueryChange, onSearchNavigate, onSearchOpen, selectMode = null, triggerHints, busy = false, onEnterQueueFromEmpty, attachImageBinding, editorBinding, onDraftChange, remoteDraft, onDraftAttachmentsChange, remoteDraftAttachments, remoteSearchQuery, remoteSuggest, resolveGhostRemote, clipboardReader = readClipboardContent, suspended = false }) {
|
|
12546
13116
|
const focused = useModalAwareFocus();
|
|
12547
13117
|
const renderer = useRenderer();
|
|
12548
13118
|
const COLOR = useColors();
|
|
@@ -12614,15 +13184,37 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
12614
13184
|
}, [popupOpen, onPopupOpenChange]);
|
|
12615
13185
|
const chipStyle = useChipStyle();
|
|
12616
13186
|
useChipHighlights(textareaRef, completion.references, chipStyle);
|
|
13187
|
+
const onDraftChangeRef = useRef(onDraftChange);
|
|
13188
|
+
onDraftChangeRef.current = onDraftChange;
|
|
13189
|
+
const applyingRemoteDraftRef = useRef(false);
|
|
12617
13190
|
const [shellMode, setShellMode] = useState(false);
|
|
12618
13191
|
const shellModeRef = useRef(false);
|
|
12619
13192
|
const searchModeRef = useRef(searchMode);
|
|
12620
13193
|
const previousSearchModeRef = useRef(searchMode);
|
|
12621
13194
|
const preSearchTextRef = useRef(null);
|
|
13195
|
+
/**
|
|
13196
|
+
* Shell flag parked alongside `preSearchTextRef`: a remote draft that
|
|
13197
|
+
* arrives WHILE searching carries its mode out-of-band — dropping it
|
|
13198
|
+
* would restore the text as a normal prompt after search exit while
|
|
13199
|
+
* every other window shows shell mode. `null` = keep the current mode
|
|
13200
|
+
* (the local park path, where shell mode was never touched).
|
|
13201
|
+
*/
|
|
13202
|
+
const preSearchShellRef = useRef(null);
|
|
12622
13203
|
/** Last text pushed into bufferState — distinguishes typed sentinels from bulk sets. */
|
|
12623
13204
|
const lastSyncedTextRef = useRef("");
|
|
12624
13205
|
searchModeRef.current = searchMode;
|
|
12625
13206
|
shellModeRef.current = shellMode;
|
|
13207
|
+
/**
|
|
13208
|
+
* Flip shell mode with the ref mirror updated SYNCHRONOUSLY — callers
|
|
13209
|
+
* that mutate the buffer and re-run `syncBuffer` in the same tick
|
|
13210
|
+
* (history cycling) must not race the render-time ref assignment.
|
|
13211
|
+
*/
|
|
13212
|
+
const setShellModeNow = useCallback((on) => {
|
|
13213
|
+
const changed = shellModeRef.current !== on;
|
|
13214
|
+
shellModeRef.current = on;
|
|
13215
|
+
setShellMode(on);
|
|
13216
|
+
if (changed && !applyingRemoteDraftRef.current && !searchModeRef.current) onDraftChangeRef.current?.(textareaRef.current?.plainText ?? "", on);
|
|
13217
|
+
}, []);
|
|
12626
13218
|
useEffect(() => {
|
|
12627
13219
|
const textarea = textareaRef.current;
|
|
12628
13220
|
if (!previousSearchModeRef.current && searchMode) {
|
|
@@ -12638,6 +13230,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
12638
13230
|
}
|
|
12639
13231
|
} else if (previousSearchModeRef.current && !searchMode) {
|
|
12640
13232
|
const restored = preSearchTextRef.current ?? "";
|
|
13233
|
+
if (preSearchShellRef.current !== null && preSearchShellRef.current !== shellModeRef.current) setShellModeNow(preSearchShellRef.current);
|
|
12641
13234
|
textarea?.setText(restored);
|
|
12642
13235
|
lastSyncedTextRef.current = restored;
|
|
12643
13236
|
setBufferState({
|
|
@@ -12645,18 +13238,10 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
12645
13238
|
cursor: restored.length
|
|
12646
13239
|
});
|
|
12647
13240
|
preSearchTextRef.current = null;
|
|
13241
|
+
preSearchShellRef.current = null;
|
|
12648
13242
|
}
|
|
12649
13243
|
previousSearchModeRef.current = searchMode;
|
|
12650
13244
|
}, [searchMode, onSearchQueryChange]);
|
|
12651
|
-
/**
|
|
12652
|
-
* Flip shell mode with the ref mirror updated SYNCHRONOUSLY — callers
|
|
12653
|
-
* that mutate the buffer and re-run `syncBuffer` in the same tick
|
|
12654
|
-
* (history cycling) must not race the render-time ref assignment.
|
|
12655
|
-
*/
|
|
12656
|
-
const setShellModeNow = useCallback((on) => {
|
|
12657
|
-
shellModeRef.current = on;
|
|
12658
|
-
setShellMode(on);
|
|
12659
|
-
}, []);
|
|
12660
13245
|
const exitSearchMode = useCallback(() => {
|
|
12661
13246
|
searchModeRef.current = false;
|
|
12662
13247
|
onSearchQueryChange?.("");
|
|
@@ -12694,6 +13279,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
12694
13279
|
text: ta.plainText,
|
|
12695
13280
|
cursor: ta.cursorOffset
|
|
12696
13281
|
});
|
|
13282
|
+
if (!applyingRemoteDraftRef.current && !searchModeRef.current) onDraftChangeRef.current?.(ta.plainText, shellModeRef.current);
|
|
12697
13283
|
setGhost((prev) => patchInlineGhost(prev, ta.plainText, ta.cursorOffset));
|
|
12698
13284
|
const total = ta.editorView.getTotalVirtualLineCount();
|
|
12699
13285
|
setContentLines(Math.max(MIN_CONTENT_LINES, total));
|
|
@@ -12709,14 +13295,15 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
12709
13295
|
const controller = new AbortController();
|
|
12710
13296
|
const { text, cursor } = bufferState;
|
|
12711
13297
|
const timer = setTimeout(() => {
|
|
12712
|
-
resolveInlineSuggestion(inlineProviders, {
|
|
13298
|
+
const local = () => resolveInlineSuggestion(inlineProviders, {
|
|
12713
13299
|
text,
|
|
12714
13300
|
cursor,
|
|
12715
13301
|
cwd: process.cwd(),
|
|
12716
13302
|
signal: controller.signal
|
|
12717
|
-
}).then((res) =>
|
|
13303
|
+
}).then((res) => res?.suffixes ?? []);
|
|
13304
|
+
(resolveGhostRemote ? resolveGhostRemote(text, cursor).then((res) => res === null ? local() : res.candidates?.length ? res.candidates : res.suggestion !== null ? [res.suggestion] : []).catch(() => local()) : local()).then((raw) => {
|
|
12718
13305
|
if (controller.signal.aborted) return;
|
|
12719
|
-
const suffixes =
|
|
13306
|
+
const suffixes = raw.map((s) => s.split("\n")[0]).filter((s) => s.length > 0);
|
|
12720
13307
|
setGhost(suffixes.length > 0 ? {
|
|
12721
13308
|
suffixes,
|
|
12722
13309
|
index: 0,
|
|
@@ -12736,7 +13323,8 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
12736
13323
|
selectMode,
|
|
12737
13324
|
shellMode,
|
|
12738
13325
|
searchMode,
|
|
12739
|
-
inlineProviders
|
|
13326
|
+
inlineProviders,
|
|
13327
|
+
resolveGhostRemote
|
|
12740
13328
|
]);
|
|
12741
13329
|
/**
|
|
12742
13330
|
* Open the current prompt in `$EDITOR` (git-style round-trip). The
|
|
@@ -12805,6 +13393,77 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
|
|
|
12805
13393
|
syncBuffer();
|
|
12806
13394
|
return true;
|
|
12807
13395
|
}, [completion, syncBuffer]);
|
|
13396
|
+
useEffect(() => {
|
|
13397
|
+
if (!remoteDraft) return;
|
|
13398
|
+
if (searchModeRef.current) {
|
|
13399
|
+
preSearchTextRef.current = remoteDraft.text;
|
|
13400
|
+
preSearchShellRef.current = remoteDraft.shell === true;
|
|
13401
|
+
return;
|
|
13402
|
+
}
|
|
13403
|
+
const ta = textareaRef.current;
|
|
13404
|
+
if (!ta) return;
|
|
13405
|
+
const remoteShell = remoteDraft.shell === true;
|
|
13406
|
+
if (ta.plainText === remoteDraft.text && shellModeRef.current === remoteShell) return;
|
|
13407
|
+
applyingRemoteDraftRef.current = true;
|
|
13408
|
+
try {
|
|
13409
|
+
if (shellModeRef.current !== remoteShell) setShellModeNow(remoteShell);
|
|
13410
|
+
ta.setText(remoteDraft.text);
|
|
13411
|
+
ta.cursorOffset = remoteDraft.text.length;
|
|
13412
|
+
syncBuffer();
|
|
13413
|
+
} finally {
|
|
13414
|
+
applyingRemoteDraftRef.current = false;
|
|
13415
|
+
}
|
|
13416
|
+
}, [remoteDraft?.seq]);
|
|
13417
|
+
useEffect(() => {
|
|
13418
|
+
if (!remoteSearchQuery || !searchModeRef.current) return;
|
|
13419
|
+
const ta = textareaRef.current;
|
|
13420
|
+
if (!ta || ta.plainText === remoteSearchQuery.query) return;
|
|
13421
|
+
applyingRemoteDraftRef.current = true;
|
|
13422
|
+
try {
|
|
13423
|
+
ta.setText(remoteSearchQuery.query);
|
|
13424
|
+
ta.cursorOffset = remoteSearchQuery.query.length;
|
|
13425
|
+
syncBuffer();
|
|
13426
|
+
} finally {
|
|
13427
|
+
applyingRemoteDraftRef.current = false;
|
|
13428
|
+
}
|
|
13429
|
+
}, [remoteSearchQuery?.seq]);
|
|
13430
|
+
useEffect(() => {
|
|
13431
|
+
if (!remoteSuggest) return;
|
|
13432
|
+
if (!inlineEnabled || popupOpen || selectMode !== null || shellMode || searchMode) return;
|
|
13433
|
+
const ta = textareaRef.current;
|
|
13434
|
+
if (!ta || ta.plainText !== remoteSuggest.basis) return;
|
|
13435
|
+
if (remoteSuggest.suggestion === null) {
|
|
13436
|
+
setGhost(null);
|
|
13437
|
+
return;
|
|
13438
|
+
}
|
|
13439
|
+
const suffixes = (remoteSuggest.candidates?.length ? remoteSuggest.candidates : [remoteSuggest.suggestion]).map((s) => s.split("\n")[0]).filter((s) => s.length > 0);
|
|
13440
|
+
setGhost(suffixes.length > 0 ? {
|
|
13441
|
+
suffixes,
|
|
13442
|
+
index: 0,
|
|
13443
|
+
text: remoteSuggest.basis,
|
|
13444
|
+
cursor: remoteSuggest.caret
|
|
13445
|
+
} : null);
|
|
13446
|
+
}, [remoteSuggest?.seq]);
|
|
13447
|
+
const onDraftAttachmentsChangeRef = useRef(onDraftAttachmentsChange);
|
|
13448
|
+
onDraftAttachmentsChangeRef.current = onDraftAttachmentsChange;
|
|
13449
|
+
const applyingRemoteAttachmentsRef = useRef(false);
|
|
13450
|
+
const attachmentsInitRef = useRef(true);
|
|
13451
|
+
useEffect(() => {
|
|
13452
|
+
if (attachmentsInitRef.current) {
|
|
13453
|
+
attachmentsInitRef.current = false;
|
|
13454
|
+
return;
|
|
13455
|
+
}
|
|
13456
|
+
if (applyingRemoteAttachmentsRef.current) {
|
|
13457
|
+
applyingRemoteAttachmentsRef.current = false;
|
|
13458
|
+
return;
|
|
13459
|
+
}
|
|
13460
|
+
onDraftAttachmentsChangeRef.current?.(attachments);
|
|
13461
|
+
}, [attachments]);
|
|
13462
|
+
useEffect(() => {
|
|
13463
|
+
if (!remoteDraftAttachments) return;
|
|
13464
|
+
applyingRemoteAttachmentsRef.current = true;
|
|
13465
|
+
setAttachments(remoteDraftAttachments.attachments);
|
|
13466
|
+
}, [remoteDraftAttachments?.seq]);
|
|
12808
13467
|
/** Apply decoded clipboard text through the shared file/attachment/inline policy. */
|
|
12809
13468
|
const applyTextPaste = useCallback((text) => {
|
|
12810
13469
|
const ta = textareaRef.current;
|
|
@@ -14043,102 +14702,30 @@ function statusColor(status, COLOR) {
|
|
|
14043
14702
|
}
|
|
14044
14703
|
}
|
|
14045
14704
|
//#endregion
|
|
14046
|
-
//#region src/chat/
|
|
14705
|
+
//#region src/chat/contribution-origin.ts
|
|
14047
14706
|
/**
|
|
14048
|
-
*
|
|
14707
|
+
* Union the three skill sources into the list the Skills tab shows, in the
|
|
14708
|
+
* precedence the AGENT applies — so the tab can never advertise a skill the
|
|
14709
|
+
* agent dropped, or attribute a name to the copy that lost.
|
|
14049
14710
|
*
|
|
14050
|
-
*
|
|
14051
|
-
*
|
|
14052
|
-
*
|
|
14711
|
+
* Precedence is extension > plugin > disk, and it is not arbitrary:
|
|
14712
|
+
* `resolveSkills` seeds its map in catalog order with later entries
|
|
14713
|
+
* overwriting earlier, `app.tsx` concatenates plugin skills BEFORE extension
|
|
14714
|
+
* ones, and catalog entries shadow anything discovered on disk. The reasoning
|
|
14715
|
+
* for extension-over-plugin lives at that concat: extension code is something
|
|
14716
|
+
* the user installed deliberately, whereas a project-scope plugin arrives with
|
|
14717
|
+
* a repo clone, so a cloned repo must not shadow a user's extension skill by
|
|
14718
|
+
* reusing its name.
|
|
14053
14719
|
*/
|
|
14054
|
-
function
|
|
14055
|
-
|
|
14056
|
-
|
|
14057
|
-
|
|
14058
|
-
|
|
14059
|
-
return
|
|
14720
|
+
function mergeSkillCatalog(opts) {
|
|
14721
|
+
const byName = /* @__PURE__ */ new Map();
|
|
14722
|
+
for (const skill of opts.disk ?? []) byName.set(skill.name, skill);
|
|
14723
|
+
for (const skill of opts.plugin ?? []) byName.set(skill.name, skill);
|
|
14724
|
+
for (const skill of opts.extension ?? []) byName.set(skill.name, skill);
|
|
14725
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
14060
14726
|
}
|
|
14061
14727
|
/**
|
|
14062
|
-
*
|
|
14063
|
-
*
|
|
14064
|
-
* - `providers` — filtered / ordered provider list (post-search).
|
|
14065
|
-
* - `expanded` — keys of providers whose sources are open. Keys for
|
|
14066
|
-
* providers that are no longer expandable are ignored rather
|
|
14067
|
-
* than cleaned up, so stale expansion state (a credential
|
|
14068
|
-
* removed while the modal is open) can't desync the cursor.
|
|
14069
|
-
*/
|
|
14070
|
-
function buildVisibleAuthRows(providers, expanded) {
|
|
14071
|
-
const out = [];
|
|
14072
|
-
for (const provider of providers) {
|
|
14073
|
-
const expandable = isExpandableProvider(provider);
|
|
14074
|
-
out.push({
|
|
14075
|
-
kind: "provider",
|
|
14076
|
-
provider,
|
|
14077
|
-
expandable
|
|
14078
|
-
});
|
|
14079
|
-
if (!expandable || !expanded.has(provider.key)) continue;
|
|
14080
|
-
for (const method of selectableSources(provider)) out.push({
|
|
14081
|
-
kind: "source",
|
|
14082
|
-
providerKey: provider.key,
|
|
14083
|
-
method
|
|
14084
|
-
});
|
|
14085
|
-
if (provider.envKey) {
|
|
14086
|
-
const envMethod = provider.methods.find((m) => m.source === "env");
|
|
14087
|
-
out.push({
|
|
14088
|
-
kind: "source",
|
|
14089
|
-
providerKey: provider.key,
|
|
14090
|
-
method: envMethod ?? {
|
|
14091
|
-
source: "env",
|
|
14092
|
-
detail: provider.envKey,
|
|
14093
|
-
active: false
|
|
14094
|
-
}
|
|
14095
|
-
});
|
|
14096
|
-
}
|
|
14097
|
-
}
|
|
14098
|
-
return out;
|
|
14099
|
-
}
|
|
14100
|
-
/**
|
|
14101
|
-
* Provider key a row "belongs to" — its own for a provider header, the parent's
|
|
14102
|
-
* for a source row.
|
|
14103
|
-
*/
|
|
14104
|
-
function parentProviderKey(row) {
|
|
14105
|
-
return row.kind === "provider" ? row.provider.key : row.providerKey;
|
|
14106
|
-
}
|
|
14107
|
-
/**
|
|
14108
|
-
* Index in `rows` of the header for the given provider key, or `-1`.
|
|
14109
|
-
*
|
|
14110
|
-
* Collapse handlers snap the cursor up to the parent BEFORE the row list
|
|
14111
|
-
* shrinks; without that the cursor keeps its flat index and silently lands on
|
|
14112
|
-
* whichever sibling row slid into that slot.
|
|
14113
|
-
*/
|
|
14114
|
-
function indexOfProviderRow(rows, providerKey) {
|
|
14115
|
-
return rows.findIndex((r) => r.kind === "provider" && r.provider.key === providerKey);
|
|
14116
|
-
}
|
|
14117
|
-
//#endregion
|
|
14118
|
-
//#region src/chat/contribution-origin.ts
|
|
14119
|
-
/**
|
|
14120
|
-
* Union the three skill sources into the list the Skills tab shows, in the
|
|
14121
|
-
* precedence the AGENT applies — so the tab can never advertise a skill the
|
|
14122
|
-
* agent dropped, or attribute a name to the copy that lost.
|
|
14123
|
-
*
|
|
14124
|
-
* Precedence is extension > plugin > disk, and it is not arbitrary:
|
|
14125
|
-
* `resolveSkills` seeds its map in catalog order with later entries
|
|
14126
|
-
* overwriting earlier, `app.tsx` concatenates plugin skills BEFORE extension
|
|
14127
|
-
* ones, and catalog entries shadow anything discovered on disk. The reasoning
|
|
14128
|
-
* for extension-over-plugin lives at that concat: extension code is something
|
|
14129
|
-
* the user installed deliberately, whereas a project-scope plugin arrives with
|
|
14130
|
-
* a repo clone, so a cloned repo must not shadow a user's extension skill by
|
|
14131
|
-
* reusing its name.
|
|
14132
|
-
*/
|
|
14133
|
-
function mergeSkillCatalog(opts) {
|
|
14134
|
-
const byName = /* @__PURE__ */ new Map();
|
|
14135
|
-
for (const skill of opts.disk ?? []) byName.set(skill.name, skill);
|
|
14136
|
-
for (const skill of opts.plugin ?? []) byName.set(skill.name, skill);
|
|
14137
|
-
for (const skill of opts.extension ?? []) byName.set(skill.name, skill);
|
|
14138
|
-
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
14139
|
-
}
|
|
14140
|
-
/**
|
|
14141
|
-
* Badge for a skill row, read straight off the skill's own metadata.
|
|
14728
|
+
* Badge for a skill row, read straight off the skill's own metadata.
|
|
14142
14729
|
*
|
|
14143
14730
|
* `undefined` for a skill discovered on disk — no badge means "this came from a
|
|
14144
14731
|
* directory you control", which is what makes a badge mean "something
|
|
@@ -14418,88 +15005,6 @@ function parentExtensionName(row) {
|
|
|
14418
15005
|
function indexOfExtensionRow(rows, extName) {
|
|
14419
15006
|
return rows.findIndex((r) => r.kind === "extension" && r.ext.name === extName);
|
|
14420
15007
|
}
|
|
14421
|
-
/**
|
|
14422
|
-
* Filter segments by the active query and resolve each one's expanded state.
|
|
14423
|
-
*
|
|
14424
|
-
* Deliberately does NOT auto-expand matches the way the Extensions tab does:
|
|
14425
|
-
* the search corpus includes each segment's full text, so a query typically
|
|
14426
|
-
* matches deep inside a long document — auto-expanding every hit would dump
|
|
14427
|
-
* thousands of lines into the viewport on a keystroke. The user expands the row
|
|
14428
|
-
* they care about.
|
|
14429
|
-
*/
|
|
14430
|
-
function buildVisibleInstructionRows(opts) {
|
|
14431
|
-
const { segments, expanded, queryActive, matchSegment } = opts;
|
|
14432
|
-
const out = [];
|
|
14433
|
-
for (const segment of segments) {
|
|
14434
|
-
if (queryActive && !matchSegment(segment)) continue;
|
|
14435
|
-
out.push({
|
|
14436
|
-
segment,
|
|
14437
|
-
expanded: expanded.has(segment.id)
|
|
14438
|
-
});
|
|
14439
|
-
}
|
|
14440
|
-
return out;
|
|
14441
|
-
}
|
|
14442
|
-
/**
|
|
14443
|
-
* Split a segment's text into renderable preview lines, clamped to
|
|
14444
|
-
* {@link INSTRUCTION_PREVIEW_LINE_CAP}.
|
|
14445
|
-
*
|
|
14446
|
-
* `hidden` is how many lines were dropped off the tail, so the view can render
|
|
14447
|
-
* an honest "+N more lines" marker instead of silently truncating. Tabs are
|
|
14448
|
-
* expanded to two spaces because the terminal text renderable doesn't
|
|
14449
|
-
* interpret `\t`, and `\r` is stripped so CRLF files don't leave artefacts at
|
|
14450
|
-
* every line end.
|
|
14451
|
-
*/
|
|
14452
|
-
function instructionPreviewLines(text) {
|
|
14453
|
-
const all = text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").split("\n");
|
|
14454
|
-
while (all.length > 0 && all[all.length - 1].trim() === "") all.pop();
|
|
14455
|
-
if (all.length <= 400) return {
|
|
14456
|
-
lines: all,
|
|
14457
|
-
hidden: 0
|
|
14458
|
-
};
|
|
14459
|
-
return {
|
|
14460
|
-
lines: all.slice(0, 400),
|
|
14461
|
-
hidden: all.length - 400
|
|
14462
|
-
};
|
|
14463
|
-
}
|
|
14464
|
-
/**
|
|
14465
|
-
* Short badge for a segment's origin — the "where does this come from" column.
|
|
14466
|
-
* `'file'` is intentionally the only one that reads like a location, since it's
|
|
14467
|
-
* the only origin the user can act on.
|
|
14468
|
-
*/
|
|
14469
|
-
function instructionOriginBadge(segment) {
|
|
14470
|
-
switch (segment.origin) {
|
|
14471
|
-
case "file": return "file";
|
|
14472
|
-
case "generated": return "generated";
|
|
14473
|
-
case "mcp": return "mcp server";
|
|
14474
|
-
case "built-in": return "built-in";
|
|
14475
|
-
}
|
|
14476
|
-
}
|
|
14477
|
-
//#endregion
|
|
14478
|
-
//#region src/tui/logs-view.ts
|
|
14479
|
-
/** Level order for the tab's min-level filter (matches the logger's ordering). */
|
|
14480
|
-
const LEVEL_ORDER = {
|
|
14481
|
-
debug: 0,
|
|
14482
|
-
info: 1,
|
|
14483
|
-
warn: 2,
|
|
14484
|
-
error: 3
|
|
14485
|
-
};
|
|
14486
|
-
/** The cycle order the `l` key walks through — coarsest last so it wraps back to "everything". */
|
|
14487
|
-
const LOG_LEVEL_CYCLE = [
|
|
14488
|
-
"debug",
|
|
14489
|
-
"info",
|
|
14490
|
-
"warn",
|
|
14491
|
-
"error"
|
|
14492
|
-
];
|
|
14493
|
-
/** Next min-level in {@link LOG_LEVEL_CYCLE}, wrapping around. */
|
|
14494
|
-
function nextLogLevel(level) {
|
|
14495
|
-
const i = LOG_LEVEL_CYCLE.indexOf(level);
|
|
14496
|
-
return LOG_LEVEL_CYCLE[(i + 1) % LOG_LEVEL_CYCLE.length];
|
|
14497
|
-
}
|
|
14498
|
-
/** Retain only records at or above `minLevel`. Preserves order. */
|
|
14499
|
-
function filterLogRecords(records, minLevel) {
|
|
14500
|
-
const min = LEVEL_ORDER[minLevel];
|
|
14501
|
-
return records.filter((r) => LEVEL_ORDER[r.level] >= min);
|
|
14502
|
-
}
|
|
14503
15008
|
//#endregion
|
|
14504
15009
|
//#region src/tui/settings-form-dialog.tsx
|
|
14505
15010
|
/** Shared dialog width — pickers use it to size their own ellipsed rows. */
|
|
@@ -14943,132 +15448,6 @@ function clipEnd(text, max, nameLen) {
|
|
|
14943
15448
|
rest: clipped.slice(Math.min(nameLen, clipped.length))
|
|
14944
15449
|
};
|
|
14945
15450
|
}
|
|
14946
|
-
const MCP_TRANSPORTS = [
|
|
14947
|
-
"stdio",
|
|
14948
|
-
"streamable-http",
|
|
14949
|
-
"sse"
|
|
14950
|
-
];
|
|
14951
|
-
/**
|
|
14952
|
-
* Validate + normalize the raw field values into a config entry.
|
|
14953
|
-
* Returns the first problem in row order so the dialog can surface it.
|
|
14954
|
-
*/
|
|
14955
|
-
function validateMcpForm(values) {
|
|
14956
|
-
const name = values.name.trim();
|
|
14957
|
-
if (name.length === 0) return {
|
|
14958
|
-
ok: false,
|
|
14959
|
-
error: "name is required"
|
|
14960
|
-
};
|
|
14961
|
-
if (!/^[\w.-]+$/.test(name)) return {
|
|
14962
|
-
ok: false,
|
|
14963
|
-
error: "name must be alphanumerics, dots, hyphens, underscores"
|
|
14964
|
-
};
|
|
14965
|
-
if (values.transport === "stdio") {
|
|
14966
|
-
const tokens = splitCommandLine(values.commandLine);
|
|
14967
|
-
if (tokens.length === 0) return {
|
|
14968
|
-
ok: false,
|
|
14969
|
-
error: "command is required for stdio servers"
|
|
14970
|
-
};
|
|
14971
|
-
const env = parseKeyValueLines(values.env, {
|
|
14972
|
-
label: "env",
|
|
14973
|
-
separators: ["="]
|
|
14974
|
-
});
|
|
14975
|
-
if (!env.ok) return {
|
|
14976
|
-
ok: false,
|
|
14977
|
-
error: env.error
|
|
14978
|
-
};
|
|
14979
|
-
return {
|
|
14980
|
-
ok: true,
|
|
14981
|
-
result: {
|
|
14982
|
-
entry: {
|
|
14983
|
-
name,
|
|
14984
|
-
transport: "stdio",
|
|
14985
|
-
command: tokens[0],
|
|
14986
|
-
...tokens.length > 1 ? { args: tokens.slice(1) } : {},
|
|
14987
|
-
...Object.keys(env.pairs).length > 0 ? { env: env.pairs } : {}
|
|
14988
|
-
},
|
|
14989
|
-
scope: values.scope
|
|
14990
|
-
}
|
|
14991
|
-
};
|
|
14992
|
-
}
|
|
14993
|
-
const url = values.url.trim();
|
|
14994
|
-
if (url.length === 0) return {
|
|
14995
|
-
ok: false,
|
|
14996
|
-
error: "url is required for http/sse servers"
|
|
14997
|
-
};
|
|
14998
|
-
if (!/^https?:\/\//.test(url)) return {
|
|
14999
|
-
ok: false,
|
|
15000
|
-
error: "url must start with http:// or https://"
|
|
15001
|
-
};
|
|
15002
|
-
const headers = parseKeyValueLines(values.headers, {
|
|
15003
|
-
label: "headers",
|
|
15004
|
-
separators: [":", "="]
|
|
15005
|
-
});
|
|
15006
|
-
if (!headers.ok) return {
|
|
15007
|
-
ok: false,
|
|
15008
|
-
error: headers.error
|
|
15009
|
-
};
|
|
15010
|
-
return {
|
|
15011
|
-
ok: true,
|
|
15012
|
-
result: {
|
|
15013
|
-
entry: {
|
|
15014
|
-
name,
|
|
15015
|
-
transport: values.transport,
|
|
15016
|
-
url,
|
|
15017
|
-
...Object.keys(headers.pairs).length > 0 ? { headers: headers.pairs } : {}
|
|
15018
|
-
},
|
|
15019
|
-
scope: values.scope
|
|
15020
|
-
}
|
|
15021
|
-
};
|
|
15022
|
-
}
|
|
15023
|
-
/**
|
|
15024
|
-
* Split a command line into tokens, honoring single/double quotes so
|
|
15025
|
-
* `npx -y my-server --root "/path with spaces"` round-trips. No escape
|
|
15026
|
-
* processing beyond quote grouping — this is a form field, not a shell.
|
|
15027
|
-
*/
|
|
15028
|
-
function splitCommandLine(line) {
|
|
15029
|
-
const out = [];
|
|
15030
|
-
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
15031
|
-
for (let m = re.exec(line); m !== null; m = re.exec(line)) out.push(m[1] ?? m[2] ?? m[3]);
|
|
15032
|
-
return out;
|
|
15033
|
-
}
|
|
15034
|
-
/**
|
|
15035
|
-
* Parse one key/value pair PER LINE. Line-based (not whitespace-split)
|
|
15036
|
-
* because values legitimately contain spaces — `Authorization: Bearer
|
|
15037
|
-
* abc123` is the canonical headers case, and env values like
|
|
15038
|
-
* `GREETING=hello world` are equally valid. The first separator found
|
|
15039
|
-
* (in `separators` priority order, `:` before `=` for headers) splits;
|
|
15040
|
-
* the rest of the line is the value, trimmed. Blank lines skipped.
|
|
15041
|
-
*/
|
|
15042
|
-
function parseKeyValueLines(raw, opts) {
|
|
15043
|
-
const pairs = {};
|
|
15044
|
-
for (const line of raw.split("\n")) {
|
|
15045
|
-
const trimmed = line.trim();
|
|
15046
|
-
if (trimmed.length === 0) continue;
|
|
15047
|
-
let splitAt = -1;
|
|
15048
|
-
for (const sep of opts.separators) {
|
|
15049
|
-
const idx = trimmed.indexOf(sep);
|
|
15050
|
-
if (idx > 0 && (splitAt === -1 || idx < splitAt)) splitAt = idx;
|
|
15051
|
-
}
|
|
15052
|
-
if (splitAt <= 0) {
|
|
15053
|
-
const forms = opts.separators.map((s) => `KEY${s}VALUE`).join(" or ");
|
|
15054
|
-
return {
|
|
15055
|
-
ok: false,
|
|
15056
|
-
error: `${opts.label} entries must be ${forms}, one per line (got "${trimmed}")`
|
|
15057
|
-
};
|
|
15058
|
-
}
|
|
15059
|
-
const key = trimmed.slice(0, splitAt).trim();
|
|
15060
|
-
const value = trimmed.slice(splitAt + 1).trim();
|
|
15061
|
-
if (key.length === 0) return {
|
|
15062
|
-
ok: false,
|
|
15063
|
-
error: `${opts.label} entry has an empty key ("${trimmed}")`
|
|
15064
|
-
};
|
|
15065
|
-
pairs[key] = value;
|
|
15066
|
-
}
|
|
15067
|
-
return {
|
|
15068
|
-
ok: true,
|
|
15069
|
-
pairs
|
|
15070
|
-
};
|
|
15071
|
-
}
|
|
15072
15451
|
function McpAddDialog({ active, existingNames, onSubmit, onCancel }) {
|
|
15073
15452
|
const COLOR = useColors();
|
|
15074
15453
|
const [transport, setTransport] = useState("stdio");
|
|
@@ -16210,8 +16589,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
16210
16589
|
if (isCategoryTab(activeTab)) {
|
|
16211
16590
|
const it = filteredByCategory[activeTab][index];
|
|
16212
16591
|
if (!it) return;
|
|
16213
|
-
if (it.kind === "toggle")
|
|
16214
|
-
|
|
16592
|
+
if (it.kind === "toggle") {
|
|
16593
|
+
const requirement = it.requires ? SETTINGS_TOGGLES.find((t) => t.key === it.requires) : void 0;
|
|
16594
|
+
if (!requirement || settings[requirement.key]) toggleBoolean(it.key);
|
|
16595
|
+
} else if (it.kind === "choice") {
|
|
16215
16596
|
const current = settings[it.key];
|
|
16216
16597
|
const idx = it.options.findIndex((o) => o.value === current);
|
|
16217
16598
|
const next = it.options[(idx + 1) % it.options.length];
|
|
@@ -17088,15 +17469,20 @@ function GeneralList({ items, cursor, highlightBg, query, contentCols, editingKe
|
|
|
17088
17469
|
const focused = i === cursor;
|
|
17089
17470
|
const bg = focused ? highlightBg : void 0;
|
|
17090
17471
|
const id = anchorIdFor(i);
|
|
17091
|
-
if (item.kind === "toggle")
|
|
17092
|
-
|
|
17093
|
-
|
|
17094
|
-
|
|
17095
|
-
|
|
17096
|
-
|
|
17097
|
-
|
|
17098
|
-
|
|
17099
|
-
|
|
17472
|
+
if (item.kind === "toggle") {
|
|
17473
|
+
const requirement = item.requires ? SETTINGS_TOGGLES.find((t) => t.key === item.requires) : void 0;
|
|
17474
|
+
const blockedBy = requirement && !settings[requirement.key] ? requirement.label : void 0;
|
|
17475
|
+
return /* @__PURE__ */ jsx$1(ToggleRow, {
|
|
17476
|
+
onClick: (zone) => onRowClick?.(i, zone),
|
|
17477
|
+
id,
|
|
17478
|
+
label: item.label,
|
|
17479
|
+
description: truncateToCols(item.description, descCols),
|
|
17480
|
+
enabled: settings[item.key],
|
|
17481
|
+
focused,
|
|
17482
|
+
bg,
|
|
17483
|
+
...blockedBy !== void 0 ? { blockedBy } : {}
|
|
17484
|
+
}, item.key);
|
|
17485
|
+
}
|
|
17100
17486
|
if (item.kind === "choice") {
|
|
17101
17487
|
const current = settings[item.key];
|
|
17102
17488
|
const opt = item.options.find((o) => o.value === current);
|
|
@@ -18411,8 +18797,9 @@ function AuthenticationList({ view, providers, rows, cursor, highlightBg, query
|
|
|
18411
18797
|
]
|
|
18412
18798
|
});
|
|
18413
18799
|
}
|
|
18414
|
-
function ToggleRow({ id, label, description, enabled, focused, bg, indent = 0, onClick }) {
|
|
18800
|
+
function ToggleRow({ id, label, description, enabled, focused, bg, indent = 0, onClick, blockedBy }) {
|
|
18415
18801
|
const COLOR = useColors();
|
|
18802
|
+
const blocked = blockedBy !== void 0;
|
|
18416
18803
|
return /* @__PURE__ */ jsxs("box", {
|
|
18417
18804
|
onMouseDown: onClick && ((event) => {
|
|
18418
18805
|
event.stopPropagation();
|
|
@@ -18432,7 +18819,7 @@ function ToggleRow({ id, label, description, enabled, focused, bg, indent = 0, o
|
|
|
18432
18819
|
onMouseDown: onClick && ((event) => {
|
|
18433
18820
|
event.stopPropagation();
|
|
18434
18821
|
event.preventDefault();
|
|
18435
|
-
onClick(inCheckboxZone(event, 2) ? "checkbox" : "row");
|
|
18822
|
+
onClick(!blocked && inCheckboxZone(event, 2) ? "checkbox" : "row");
|
|
18436
18823
|
}),
|
|
18437
18824
|
children: [
|
|
18438
18825
|
/* @__PURE__ */ jsx$1("span", {
|
|
@@ -18440,18 +18827,18 @@ function ToggleRow({ id, label, description, enabled, focused, bg, indent = 0, o
|
|
|
18440
18827
|
children: focused ? "▶ " : " "
|
|
18441
18828
|
}),
|
|
18442
18829
|
/* @__PURE__ */ jsx$1("span", {
|
|
18443
|
-
fg: enabled ? COLOR.accent : COLOR.mute,
|
|
18444
|
-
children: enabled ? "[✓] " : "[ ] "
|
|
18830
|
+
fg: blocked ? COLOR.mute : enabled ? COLOR.accent : COLOR.mute,
|
|
18831
|
+
children: blocked ? "[–] " : enabled ? "[✓] " : "[ ] "
|
|
18445
18832
|
}),
|
|
18446
18833
|
/* @__PURE__ */ jsx$1("span", {
|
|
18447
|
-
fg: focused ? COLOR.brand : COLOR.dim,
|
|
18834
|
+
fg: blocked ? COLOR.mute : focused ? COLOR.brand : COLOR.dim,
|
|
18448
18835
|
children: label
|
|
18449
18836
|
})
|
|
18450
18837
|
]
|
|
18451
18838
|
}), /* @__PURE__ */ jsx$1("text", {
|
|
18452
18839
|
wrapMode: "none",
|
|
18453
18840
|
fg: COLOR.mute,
|
|
18454
|
-
children: `${COL_TITLE}${description}`
|
|
18841
|
+
children: `${COL_TITLE}${blocked ? `requires “${blockedBy}” — turn that on first` : description}`
|
|
18455
18842
|
})]
|
|
18456
18843
|
});
|
|
18457
18844
|
}
|
|
@@ -19020,109 +19407,7 @@ function restoreTerminalTitle(out = process.stdout) {
|
|
|
19020
19407
|
} catch {}
|
|
19021
19408
|
}
|
|
19022
19409
|
//#endregion
|
|
19023
|
-
//#region src/chat/todo-edits.ts
|
|
19024
|
-
/**
|
|
19025
|
-
* Prefix for ids minted here, distinguishing user-authored rows from the
|
|
19026
|
-
* model's own ids in a session file. Purely diagnostic — nothing branches on
|
|
19027
|
-
* it — but it makes "who added this" answerable after the fact.
|
|
19028
|
-
*/
|
|
19029
|
-
const USER_TODO_ID_PREFIX = "user-";
|
|
19030
|
-
/**
|
|
19031
|
-
* Mint an id that cannot collide with an existing row.
|
|
19032
|
-
*
|
|
19033
|
-
* Counter-based rather than random: a stable, readable id keeps session diffs
|
|
19034
|
-
* legible, and the collision check makes it safe against ids the model already
|
|
19035
|
-
* used (including previous `user-*` ones after a reload).
|
|
19036
|
-
*/
|
|
19037
|
-
function nextTodoId(items) {
|
|
19038
|
-
const taken = new Set(items.map((i) => i.id));
|
|
19039
|
-
for (let n = 1;; n++) {
|
|
19040
|
-
const id = `${USER_TODO_ID_PREFIX}${n}`;
|
|
19041
|
-
if (!taken.has(id)) return id;
|
|
19042
|
-
}
|
|
19043
|
-
}
|
|
19044
|
-
/**
|
|
19045
|
-
* Append a todo. Returns the list unchanged when `content` is blank — an empty
|
|
19046
|
-
* row is never what the user meant, and rejecting it here means every caller
|
|
19047
|
-
* (keyboard, click, future GUI) gets the same guard.
|
|
19048
|
-
*/
|
|
19049
|
-
function addTodo(items, content) {
|
|
19050
|
-
const trimmed = content.trim();
|
|
19051
|
-
if (!trimmed) return [...items];
|
|
19052
|
-
return [...items, {
|
|
19053
|
-
id: nextTodoId(items),
|
|
19054
|
-
content: trimmed,
|
|
19055
|
-
status: "pending"
|
|
19056
|
-
}];
|
|
19057
|
-
}
|
|
19058
|
-
/**
|
|
19059
|
-
* Replace one todo's text. Blank content is rejected (see {@link addTodo}); an
|
|
19060
|
-
* unknown id is a no-op rather than an error, because the list can be replaced
|
|
19061
|
-
* by a `todowrite` between render and keypress.
|
|
19062
|
-
*/
|
|
19063
|
-
function editTodo(items, id, content) {
|
|
19064
|
-
const trimmed = content.trim();
|
|
19065
|
-
if (!trimmed) return [...items];
|
|
19066
|
-
return items.map((item) => item.id === id ? {
|
|
19067
|
-
...item,
|
|
19068
|
-
content: trimmed
|
|
19069
|
-
} : item);
|
|
19070
|
-
}
|
|
19071
|
-
/** Drop one todo. Unknown id is a no-op, same rationale as {@link editTodo}. */
|
|
19072
|
-
function removeTodo(items, id) {
|
|
19073
|
-
return items.filter((item) => item.id !== id);
|
|
19074
|
-
}
|
|
19075
|
-
/** Drop everything. Separate from `removeTodo` so the caller's intent is explicit at the call site. */
|
|
19076
|
-
function clearTodos() {
|
|
19077
|
-
return [];
|
|
19078
|
-
}
|
|
19079
|
-
/**
|
|
19080
|
-
* Advance a todo's status one step: pending → in_progress → completed →
|
|
19081
|
-
* pending. `cancelled` also lands on `pending`, so a dropped item can be
|
|
19082
|
-
* revived without a dedicated key.
|
|
19083
|
-
*
|
|
19084
|
-
* Cycling INTO `in_progress` demotes any other in-progress row to `pending`:
|
|
19085
|
-
* the list's whole purpose is "what is being worked on right now", and two
|
|
19086
|
-
* live rows make that unanswerable — `TodoIndicator`'s successor
|
|
19087
|
-
* (`todoProgressLabel`) reads the first one and would silently ignore the rest.
|
|
19088
|
-
*/
|
|
19089
|
-
function cycleTodoStatus(items, id) {
|
|
19090
|
-
const target = items.find((item) => item.id === id);
|
|
19091
|
-
if (!target) return [...items];
|
|
19092
|
-
const next = nextStatus(target.status);
|
|
19093
|
-
return items.map((item) => {
|
|
19094
|
-
if (item.id === id) return {
|
|
19095
|
-
...item,
|
|
19096
|
-
status: next
|
|
19097
|
-
};
|
|
19098
|
-
if (next === "in_progress" && item.status === "in_progress") return {
|
|
19099
|
-
...item,
|
|
19100
|
-
status: "pending"
|
|
19101
|
-
};
|
|
19102
|
-
return item;
|
|
19103
|
-
});
|
|
19104
|
-
}
|
|
19105
|
-
function nextStatus(status) {
|
|
19106
|
-
switch (status) {
|
|
19107
|
-
case "pending": return "in_progress";
|
|
19108
|
-
case "in_progress": return "completed";
|
|
19109
|
-
default: return "pending";
|
|
19110
|
-
}
|
|
19111
|
-
}
|
|
19112
|
-
//#endregion
|
|
19113
19410
|
//#region src/tui/todos-modal.tsx
|
|
19114
|
-
/**
|
|
19115
|
-
* Steering text for a "retry this todo" request.
|
|
19116
|
-
*
|
|
19117
|
-
* Quotes the content so a model reading it can match the row verbatim against
|
|
19118
|
-
* its own list, and names the status because "retry" means different things
|
|
19119
|
-
* for a row it believes is `completed` versus one it abandoned as `cancelled`.
|
|
19120
|
-
* Pure + exported so the wording is pinned by a test rather than living only
|
|
19121
|
-
* inside a JSX callback.
|
|
19122
|
-
*/
|
|
19123
|
-
function buildTodoRetryMessage(item) {
|
|
19124
|
-
return `Retry this todo (currently ${item.status}): "${item.content}"`;
|
|
19125
|
-
}
|
|
19126
19411
|
/** Floor on the modal height — keeps the header + at least a few rows visible on tiny terminals. */
|
|
19127
19412
|
const MIN_MODAL_HEIGHT = 12;
|
|
19128
19413
|
/** Ceiling on the modal height — prevents a giant list from blanket-filling a tall window. */
|
|
@@ -19197,7 +19482,7 @@ function TodoRow({ item, rowId, focused, onClick }) {
|
|
|
19197
19482
|
})
|
|
19198
19483
|
});
|
|
19199
19484
|
}
|
|
19200
|
-
function TodosModal({
|
|
19485
|
+
function TodosModal({ getSession, subscribe, onCommit, onSteer }) {
|
|
19201
19486
|
const COLOR = useColors();
|
|
19202
19487
|
const modal = useModal();
|
|
19203
19488
|
const { height: termHeight } = useTerminalDimensions();
|
|
@@ -19205,12 +19490,8 @@ function TodosModal({ session, agent, onSteer }) {
|
|
|
19205
19490
|
const [cursorRaw, setCursorRaw] = useState(0);
|
|
19206
19491
|
const [mode, setMode] = useState("list");
|
|
19207
19492
|
const [draft, setDraft] = useState("");
|
|
19208
|
-
useEffect(() =>
|
|
19209
|
-
|
|
19210
|
-
return agent.hooks.hook("tool:after", (ctx) => {
|
|
19211
|
-
if (ctx.name === "todowrite") setTick((t) => t + 1);
|
|
19212
|
-
});
|
|
19213
|
-
}, [agent]);
|
|
19493
|
+
useEffect(() => subscribe(() => setTick((t) => t + 1)), [subscribe]);
|
|
19494
|
+
const session = getSession();
|
|
19214
19495
|
const state = useActiveTodos(session);
|
|
19215
19496
|
const scrollRef = useRef(null);
|
|
19216
19497
|
const inProgressId = state.inProgress?.id ?? null;
|
|
@@ -19233,14 +19514,13 @@ function TodosModal({ session, agent, onSteer }) {
|
|
|
19233
19514
|
* Write a transformed list back to the run.
|
|
19234
19515
|
*
|
|
19235
19516
|
* Editing while the ARCHIVE is on screen promotes it back to the live slot,
|
|
19236
|
-
* which is the only coherent option:
|
|
19237
|
-
* an edit the user just made must be the list that's displayed next.
|
|
19517
|
+
* which is the only coherent option: the live slot is what the model reads,
|
|
19518
|
+
* and an edit the user just made must be the list that's displayed next.
|
|
19238
19519
|
*/
|
|
19239
19520
|
const commit = useCallback((next) => {
|
|
19240
|
-
|
|
19241
|
-
setTodosForRun(session, state.runId, next, { clearArchive: true });
|
|
19521
|
+
onCommit(next);
|
|
19242
19522
|
setTick((t) => t + 1);
|
|
19243
|
-
}, [
|
|
19523
|
+
}, [onCommit]);
|
|
19244
19524
|
const editable = session !== null && state.runId !== null;
|
|
19245
19525
|
/** Enter add / edit mode, seeding the draft from the focused row for edits. */
|
|
19246
19526
|
const beginEdit = useCallback((next) => {
|
|
@@ -19503,6 +19783,18 @@ function CountsBadge({ items }) {
|
|
|
19503
19783
|
});
|
|
19504
19784
|
}
|
|
19505
19785
|
//#endregion
|
|
19786
|
+
//#region src/tui/tui-bus.ts
|
|
19787
|
+
let holder$3 = null;
|
|
19788
|
+
/** Initialize (or return) the process-wide extension bus. Idempotent. */
|
|
19789
|
+
function initTuiBus() {
|
|
19790
|
+
holder$3 ??= createExtensionBus();
|
|
19791
|
+
return holder$3;
|
|
19792
|
+
}
|
|
19793
|
+
/** The bus, or null when `runTui` hasn't wired it (bare embedders). */
|
|
19794
|
+
function getTuiBus() {
|
|
19795
|
+
return holder$3;
|
|
19796
|
+
}
|
|
19797
|
+
//#endregion
|
|
19506
19798
|
//#region src/tui/tui-host-runtime.ts
|
|
19507
19799
|
let holder$2 = null;
|
|
19508
19800
|
/** Initialize (or return) the process-wide bridge. Idempotent. */
|
|
@@ -20130,32 +20422,10 @@ const BAR_WIDTH = 12;
|
|
|
20130
20422
|
/** Right-hand columns, padded so percents and resets align down the panel. */
|
|
20131
20423
|
const PERCENT_COL = 6;
|
|
20132
20424
|
const TRAILING_COL = 16;
|
|
20133
|
-
/** Percent thresholds
|
|
20425
|
+
/** Percent thresholds → palette. The thresholds themselves are shared with the GUI via `usageTone`. */
|
|
20134
20426
|
function usageColor(usedPercent, COLOR) {
|
|
20135
|
-
|
|
20136
|
-
|
|
20137
|
-
return COLOR.brand;
|
|
20138
|
-
}
|
|
20139
|
-
/**
|
|
20140
|
-
* Time to reset, coarsened to whole days past 24h. `null` when the provider
|
|
20141
|
-
* reported no reset, `resetting` once it is due.
|
|
20142
|
-
*
|
|
20143
|
-
* Every step rounds DOWN, like the footer's percent: 36h reads as `1d`, not
|
|
20144
|
-
* `2d`. Overstating the headroom left on a window is the wrong direction to
|
|
20145
|
-
* be wrong in.
|
|
20146
|
-
*/
|
|
20147
|
-
function resetLabel(resetsAt, now = Date.now()) {
|
|
20148
|
-
if (resetsAt === null) return null;
|
|
20149
|
-
const remaining = resetsAt - now;
|
|
20150
|
-
if (remaining <= 0) return "resetting";
|
|
20151
|
-
const minutes = Math.floor(remaining / 6e4);
|
|
20152
|
-
if (minutes < 60) return `resets in ${Math.max(1, minutes)}m`;
|
|
20153
|
-
const hours = Math.floor(minutes / 60);
|
|
20154
|
-
if (hours < 24) {
|
|
20155
|
-
const remMinutes = minutes % 60;
|
|
20156
|
-
return `resets in ${hours}h${remMinutes > 0 ? `${remMinutes}m` : ""}`;
|
|
20157
|
-
}
|
|
20158
|
-
return `resets in ${Math.max(1, Math.floor(hours / 24))}d`;
|
|
20427
|
+
const tone = usageTone(usedPercent);
|
|
20428
|
+
return tone === "error" ? COLOR.error : tone === "warn" ? COLOR.warn : COLOR.brand;
|
|
20159
20429
|
}
|
|
20160
20430
|
function UsagePanelModal({ dataDir, registry, activeProviderKey, autoLoad = true }) {
|
|
20161
20431
|
const COLOR = useColors();
|
|
@@ -20342,15 +20612,6 @@ function emptyStateLine({ loading, loaded, envOptedOut, autoLoad }) {
|
|
|
20342
20612
|
if (!autoLoad && !loaded) return "usage checks are off · ctrl+r fetches once";
|
|
20343
20613
|
return "no signed-in provider reports usage";
|
|
20344
20614
|
}
|
|
20345
|
-
/** `20.00 / 25.00`, or just the remainder when no total is reported. */
|
|
20346
|
-
function formatBalance(credits) {
|
|
20347
|
-
const remaining = formatCredits(credits.remaining);
|
|
20348
|
-
return credits.total === null ? remaining : `${remaining} / ${formatCredits(credits.total)}`;
|
|
20349
|
-
}
|
|
20350
|
-
/** Balances are dollars for OpenRouter and opaque units elsewhere. */
|
|
20351
|
-
function formatCredits(value) {
|
|
20352
|
-
return value >= 100 ? value.toFixed(0) : value.toFixed(2);
|
|
20353
|
-
}
|
|
20354
20615
|
//#endregion
|
|
20355
20616
|
//#region src/tui/app.tsx
|
|
20356
20617
|
const BUILTIN_COMPLETION_PROVIDER_IDS = /* @__PURE__ */ new Set([
|
|
@@ -20557,10 +20818,24 @@ function App({ config }) {
|
|
|
20557
20818
|
...DEFAULT_SETTINGS,
|
|
20558
20819
|
...config.initialSettings
|
|
20559
20820
|
}), [config.initialSettings]),
|
|
20560
|
-
onChange: useCallback((settings) =>
|
|
20561
|
-
|
|
20562
|
-
|
|
20563
|
-
|
|
20821
|
+
onChange: useCallback((settings) => {
|
|
20822
|
+
if (settings.useDaemon && isDaemonConnected()) {
|
|
20823
|
+
const state = config.stateStore.load();
|
|
20824
|
+
const merged = {
|
|
20825
|
+
...state.settings ?? {},
|
|
20826
|
+
useDaemon: settings.useDaemon
|
|
20827
|
+
};
|
|
20828
|
+
config.stateStore.save({
|
|
20829
|
+
...state,
|
|
20830
|
+
settings: merged
|
|
20831
|
+
});
|
|
20832
|
+
return;
|
|
20833
|
+
}
|
|
20834
|
+
config.stateStore.save({
|
|
20835
|
+
...config.stateStore.load(),
|
|
20836
|
+
settings
|
|
20837
|
+
});
|
|
20838
|
+
}, [config.stateStore]),
|
|
20564
20839
|
children: /* @__PURE__ */ jsx$1(ThemedShell, {})
|
|
20565
20840
|
})
|
|
20566
20841
|
});
|
|
@@ -20614,7 +20889,7 @@ function AppShell() {
|
|
|
20614
20889
|
bootTick("AppShell:first-effect (post-first-paint)");
|
|
20615
20890
|
}, []);
|
|
20616
20891
|
const queue = useSafeModeQueue();
|
|
20617
|
-
const { requestApproval, resolveHead, denyAll } = useSafeModeActions();
|
|
20892
|
+
const { requestApproval, resolveHead, denyAll, dropByCorrelation: dropApprovalByCorrelation } = useSafeModeActions();
|
|
20618
20893
|
const pendingApproval = queue[0] ?? null;
|
|
20619
20894
|
const interactionsQueue = useInteractionsQueue();
|
|
20620
20895
|
const interactions = useInteractionsActions();
|
|
@@ -20759,9 +21034,16 @@ function AppShell() {
|
|
|
20759
21034
|
]);
|
|
20760
21035
|
const runCapsRef = useRef(runCaps);
|
|
20761
21036
|
runCapsRef.current = runCaps;
|
|
21037
|
+
/**
|
|
21038
|
+
* When THIS window's last local run settled, per session — exempts the
|
|
21039
|
+
* cross-process run guard from refusing our own settle-lagged
|
|
21040
|
+
* `'running'` marker (mirror of the host's `lastRunEndedAt`).
|
|
21041
|
+
*/
|
|
21042
|
+
const lastLocalRunEndRef = useRef(null);
|
|
20762
21043
|
const lastFinishReasonRef = useRef(void 0);
|
|
20763
21044
|
const providerRegistryRef = useRef(providerRegistry);
|
|
20764
21045
|
providerRegistryRef.current = providerRegistry;
|
|
21046
|
+
const availableProvidersRef = useRef([]);
|
|
20765
21047
|
/**
|
|
20766
21048
|
* Loop-native `behavior.autoCompact` for a run — honors the user's
|
|
20767
21049
|
* enable/threshold settings and injects the picked model's context window.
|
|
@@ -20938,6 +21220,7 @@ function AppShell() {
|
|
|
20938
21220
|
...getTuiHud() ? { hud: getTuiHud() } : {},
|
|
20939
21221
|
...getTuiPromptBridge() ? { promptBridge: getTuiPromptBridge() } : {},
|
|
20940
21222
|
...getTuiHostRuntime() ? { hostRuntime: getTuiHostRuntime() } : {},
|
|
21223
|
+
...getTuiBus() ? { bus: getTuiBus() } : {},
|
|
20941
21224
|
readSettings: readLiveSettings
|
|
20942
21225
|
}), [
|
|
20943
21226
|
config.prefix,
|
|
@@ -21036,6 +21319,8 @@ function AppShell() {
|
|
|
21036
21319
|
extensionRegistry,
|
|
21037
21320
|
enabledExtensionsSet
|
|
21038
21321
|
]);
|
|
21322
|
+
const liveAgentRegistryRef = useRef(liveAgentRegistry);
|
|
21323
|
+
liveAgentRegistryRef.current = liveAgentRegistry;
|
|
21039
21324
|
/**
|
|
21040
21325
|
* Single source of truth for "should this call execute?". Three short-circuits:
|
|
21041
21326
|
*
|
|
@@ -21449,7 +21734,270 @@ function AppShell() {
|
|
|
21449
21734
|
const queueSelectionIndexRef = useRef(null);
|
|
21450
21735
|
queueSelectionIndexRef.current = queueSelectionIndex;
|
|
21451
21736
|
const agentRef = useRef(null);
|
|
21737
|
+
/**
|
|
21738
|
+
* Daemon runtime controller (experimental, `useDaemon` setting). When
|
|
21739
|
+
* set, execution lives in the shared daemon and `agentRef` stays null —
|
|
21740
|
+
* every `agentRef.current?.` consumer degrades soft by construction.
|
|
21741
|
+
* Mutually exclusive with `agentRef` per activation.
|
|
21742
|
+
*/
|
|
21743
|
+
const daemonRef = useRef(null);
|
|
21744
|
+
useEffect(() => getTuiBus()?.onEmit((envelope) => {
|
|
21745
|
+
daemonRef.current?.emitExtension(envelope);
|
|
21746
|
+
}), []);
|
|
21747
|
+
/**
|
|
21748
|
+
* Suppresses the settings-publish effect for exactly one commit — set
|
|
21749
|
+
* when a `settings:changed` broadcast is being applied locally, so the
|
|
21750
|
+
* apply doesn't echo straight back to the daemon.
|
|
21751
|
+
*/
|
|
21752
|
+
const suppressSettingsPublishRef = useRef(false);
|
|
21753
|
+
const settingsPublishInitRef = useRef(true);
|
|
21754
|
+
const [remoteDraft, setRemoteDraft] = useState(null);
|
|
21755
|
+
const draftSeqRef = useRef(0);
|
|
21756
|
+
const [remoteDraftAttachments, setRemoteDraftAttachments] = useState(null);
|
|
21757
|
+
const draftAttachmentsSeqRef = useRef(0);
|
|
21758
|
+
/**
|
|
21759
|
+
* Remote inline-suggest mirror (daemon mode) — the daemon resolved
|
|
21760
|
+
* ANOTHER window's ghost query on this session (origin-excluded like
|
|
21761
|
+
* drafts). Local `seq` keys the PromptBlock apply; the basis guard
|
|
21762
|
+
* lives in PromptBlock.
|
|
21763
|
+
*/
|
|
21764
|
+
const [remoteSuggest, setRemoteSuggest] = useState(null);
|
|
21765
|
+
const remoteSuggestSeqRef = useRef(0);
|
|
21766
|
+
/**
|
|
21767
|
+
* Daemon-side ghost resolver for PromptBlock — the typing window's
|
|
21768
|
+
* request is the single shared computation. Resolves `null` in local
|
|
21769
|
+
* mode and on older daemons (method not found), which tells
|
|
21770
|
+
* PromptBlock to run today's local compute path unchanged.
|
|
21771
|
+
*/
|
|
21772
|
+
const resolveGhostRemote = useCallback(async (text, cursor) => {
|
|
21773
|
+
const daemon = daemonRef.current;
|
|
21774
|
+
if (!daemon) return null;
|
|
21775
|
+
return daemon.resolveSuggestion(text, cursor);
|
|
21776
|
+
}, []);
|
|
21777
|
+
/**
|
|
21778
|
+
* Composing-attachment publish (daemon mode) — payloads ride the wire so
|
|
21779
|
+
* a remote window can submit the shared draft attachments-and-all.
|
|
21780
|
+
* Change-driven (paste/drop/remove), not per-keystroke, so no debounce
|
|
21781
|
+
* concerns. No-op in local mode.
|
|
21782
|
+
*/
|
|
21783
|
+
const onPromptDraftAttachmentsChange = useCallback((attachments) => {
|
|
21784
|
+
daemonRef.current?.publishDraftAttachments(attachments.map((att) => ({
|
|
21785
|
+
mediaType: att.mediaType,
|
|
21786
|
+
data: att.content.toString("base64"),
|
|
21787
|
+
name: att.name
|
|
21788
|
+
})));
|
|
21789
|
+
}, []);
|
|
21790
|
+
/** Parallel id list for the daemon-mirrored queue (index-aligned with `messageQueue`). */
|
|
21791
|
+
const daemonQueueIdsRef = useRef([]);
|
|
21792
|
+
/**
|
|
21793
|
+
* Live presence from the daemon — sessions with attached windows and
|
|
21794
|
+
* their run / awaiting-input state. Feeds the session list's "active"
|
|
21795
|
+
* scope and per-row status icons. Empty in local mode.
|
|
21796
|
+
*/
|
|
21797
|
+
const [sessionPresence, setSessionPresence] = useState(/* @__PURE__ */ new Map());
|
|
21798
|
+
useEffect(() => {
|
|
21799
|
+
if (!settings.useDaemon || isDaemonDisabledByEnv()) {
|
|
21800
|
+
setSessionPresence(/* @__PURE__ */ new Map());
|
|
21801
|
+
return;
|
|
21802
|
+
}
|
|
21803
|
+
let cancelled = false;
|
|
21804
|
+
let client = null;
|
|
21805
|
+
let timer = null;
|
|
21806
|
+
setDaemonHealth("connecting");
|
|
21807
|
+
const applyRows = (rows) => {
|
|
21808
|
+
if (!cancelled) setSessionPresence(new Map(rows.map((row) => [row.sessionId, row])));
|
|
21809
|
+
};
|
|
21810
|
+
const connect = async () => {
|
|
21811
|
+
if (cancelled) return;
|
|
21812
|
+
try {
|
|
21813
|
+
const socketPath = defaultSocketPath();
|
|
21814
|
+
if (!await probeSocket(socketPath)) throw new Error("daemon not listening");
|
|
21815
|
+
const connected = await connectIpcClient({
|
|
21816
|
+
socketPath,
|
|
21817
|
+
appVersion: "tui-presence",
|
|
21818
|
+
drainOnMismatch: false,
|
|
21819
|
+
onClose: () => {
|
|
21820
|
+
if (cancelled) return;
|
|
21821
|
+
client = null;
|
|
21822
|
+
setSessionPresence(/* @__PURE__ */ new Map());
|
|
21823
|
+
timer = setTimeout(() => void connect(), 3e3);
|
|
21824
|
+
}
|
|
21825
|
+
});
|
|
21826
|
+
if (cancelled) {
|
|
21827
|
+
connected.close();
|
|
21828
|
+
return;
|
|
21829
|
+
}
|
|
21830
|
+
client = connected;
|
|
21831
|
+
connected.onSignal((signal) => {
|
|
21832
|
+
if (signal.kind === "presence:changed") applyRows(signal.sessions);
|
|
21833
|
+
});
|
|
21834
|
+
setDaemonHealth("connected");
|
|
21835
|
+
try {
|
|
21836
|
+
applyRows(await connected.request("presence.list", {}));
|
|
21837
|
+
} catch {}
|
|
21838
|
+
} catch {
|
|
21839
|
+
if (!cancelled) setDaemonHealth("disconnected");
|
|
21840
|
+
timer = setTimeout(() => void connect(), 3e3);
|
|
21841
|
+
}
|
|
21842
|
+
};
|
|
21843
|
+
connect();
|
|
21844
|
+
return () => {
|
|
21845
|
+
cancelled = true;
|
|
21846
|
+
if (timer) clearTimeout(timer);
|
|
21847
|
+
client?.close();
|
|
21848
|
+
setDaemonHealth("disconnected");
|
|
21849
|
+
};
|
|
21850
|
+
}, [settings.useDaemon]);
|
|
21851
|
+
/**
|
|
21852
|
+
* Model-pick → session-metadata cabling (daemon mode). Seeded at
|
|
21853
|
+
* activation with the CURRENT pick so the push effect only fires on a
|
|
21854
|
+
* genuine user change — pushing at mount would dispose a possibly-live
|
|
21855
|
+
* daemon agent for nothing.
|
|
21856
|
+
*/
|
|
21857
|
+
const lastPushedModelRef = useRef(null);
|
|
21858
|
+
/**
|
|
21859
|
+
* Forward ref to {@link activateSession} — declared early so daemon
|
|
21860
|
+
* callbacks (reconnect-on-disconnect) can reference it lexically before
|
|
21861
|
+
* the callback itself is defined below.
|
|
21862
|
+
*/
|
|
21863
|
+
const activateSessionRef = useRef(async () => {});
|
|
21864
|
+
const activationGenerationRef = useRef(0);
|
|
21865
|
+
useEffect(() => {
|
|
21866
|
+
const daemon = daemonRef.current;
|
|
21867
|
+
if (!daemon || !picked) return;
|
|
21868
|
+
const tuple = JSON.stringify([
|
|
21869
|
+
picked.provider.key,
|
|
21870
|
+
picked.model,
|
|
21871
|
+
picked.effort ?? null,
|
|
21872
|
+
picked.modelOptions ?? null
|
|
21873
|
+
]);
|
|
21874
|
+
if (lastPushedModelRef.current === tuple) return;
|
|
21875
|
+
lastPushedModelRef.current = tuple;
|
|
21876
|
+
daemon.applyModel({
|
|
21877
|
+
providerKey: picked.provider.key,
|
|
21878
|
+
modelId: picked.model,
|
|
21879
|
+
...picked.effort ? { effort: picked.effort } : {},
|
|
21880
|
+
...picked.modelOptions ? { modelOptions: picked.modelOptions } : {}
|
|
21881
|
+
});
|
|
21882
|
+
}, [picked]);
|
|
21883
|
+
/** Remote search-query mirror for the PromptBlock input (seq-keyed apply). */
|
|
21884
|
+
const [remoteSearchQuery, setRemoteSearchQuery] = useState(null);
|
|
21885
|
+
const remoteSearchSeqRef = useRef(0);
|
|
21886
|
+
const searchPublishInitRef = useRef(true);
|
|
21887
|
+
useEffect(() => {
|
|
21888
|
+
if (searchPublishInitRef.current) {
|
|
21889
|
+
searchPublishInitRef.current = false;
|
|
21890
|
+
return;
|
|
21891
|
+
}
|
|
21892
|
+
daemonRef.current?.publishSearch(transcriptSearchMode, transcriptSearchQuery);
|
|
21893
|
+
}, [transcriptSearchMode, transcriptSearchQuery]);
|
|
21894
|
+
/**
|
|
21895
|
+
* Local-edit → daemon draft publish, on EVERY keystroke. No debounce:
|
|
21896
|
+
* `publishDraft` is a fire-and-forget JSON-RPC notification on a local
|
|
21897
|
+
* unix socket (no ids, no response), so per-key cost is negligible and
|
|
21898
|
+
* other windows mirror the draft with keystroke latency. Synchronous
|
|
21899
|
+
* dispatch also removes the timer/session-switch race the debounce
|
|
21900
|
+
* needed guards for. No-op in local mode.
|
|
21901
|
+
*/
|
|
21902
|
+
const onPromptDraftChange = useCallback((text, shell) => {
|
|
21903
|
+
daemonRef.current?.publishDraft(text, shell);
|
|
21904
|
+
}, []);
|
|
21905
|
+
const prevSettingsRef = useRef(settings);
|
|
21906
|
+
useEffect(() => {
|
|
21907
|
+
const prev = prevSettingsRef.current;
|
|
21908
|
+
prevSettingsRef.current = settings;
|
|
21909
|
+
if (settingsPublishInitRef.current) {
|
|
21910
|
+
settingsPublishInitRef.current = false;
|
|
21911
|
+
return;
|
|
21912
|
+
}
|
|
21913
|
+
if (suppressSettingsPublishRef.current) {
|
|
21914
|
+
suppressSettingsPublishRef.current = false;
|
|
21915
|
+
return;
|
|
21916
|
+
}
|
|
21917
|
+
const patch = {};
|
|
21918
|
+
for (const key of Object.keys(settings)) if (JSON.stringify(prev[key]) !== JSON.stringify(settings[key])) patch[key] = settings[key];
|
|
21919
|
+
if (Object.keys(patch).length > 0) daemonRef.current?.publishSettings(patch);
|
|
21920
|
+
}, [settings]);
|
|
21452
21921
|
const sessionRef = useRef(null);
|
|
21922
|
+
/**
|
|
21923
|
+
* Todo-change listeners, notified by BOTH runtimes.
|
|
21924
|
+
*
|
|
21925
|
+
* Runtime-agnostic on purpose. Binding local-vs-daemon at subscribe time
|
|
21926
|
+
* looked equivalent but wasn't: `agentRef` is null between teardown and
|
|
21927
|
+
* rebuild, and the `ctrl+t` guard only requires a chat screen with a session
|
|
21928
|
+
* — so opening the modal in that window registered against whichever runtime
|
|
21929
|
+
* happened to exist at mount. A local open with `agentRef` momentarily null
|
|
21930
|
+
* bound to nothing, and an agent rebuild under an open modal left the
|
|
21931
|
+
* subscription on the dead agent. Both reproduce the frozen-modal bug this
|
|
21932
|
+
* plumbing exists to fix.
|
|
21933
|
+
*/
|
|
21934
|
+
const todosListenersRef = useRef(/* @__PURE__ */ new Set());
|
|
21935
|
+
/**
|
|
21936
|
+
* Monotonic token for todo-snapshot application order.
|
|
21937
|
+
*
|
|
21938
|
+
* `todowrite` typically fires several times per run, and each signal starts
|
|
21939
|
+
* its own `loadSession`. Those loads can overlap and resolve out of order, in
|
|
21940
|
+
* which case the OLDER snapshot is applied last and sticks — the window keeps
|
|
21941
|
+
* rendering a stale list until an unrelated `todowrite` happens along, so it
|
|
21942
|
+
* does not self-heal. Same shape as the `suggestSeq` guard in
|
|
21943
|
+
* `runtime/daemon.ts`: stamp before the await, bail after it if a newer
|
|
21944
|
+
* application has started.
|
|
21945
|
+
*/
|
|
21946
|
+
const todosApplySeqRef = useRef(0);
|
|
21947
|
+
const [, bumpTodosRevision] = useState(0);
|
|
21948
|
+
const notifyTodosChanged = useCallback(() => {
|
|
21949
|
+
bumpTodosRevision((n) => n + 1);
|
|
21950
|
+
for (const listener of todosListenersRef.current) listener();
|
|
21951
|
+
}, []);
|
|
21952
|
+
const subscribeTodos = useCallback((onChange) => {
|
|
21953
|
+
todosListenersRef.current.add(onChange);
|
|
21954
|
+
return () => {
|
|
21955
|
+
todosListenersRef.current.delete(onChange);
|
|
21956
|
+
};
|
|
21957
|
+
}, []);
|
|
21958
|
+
const commitTodos = useCallback((items) => {
|
|
21959
|
+
const session = sessionRef.current;
|
|
21960
|
+
if (!session) return;
|
|
21961
|
+
const runId = selectActiveTodos(session).runId;
|
|
21962
|
+
if (!runId) return;
|
|
21963
|
+
setTodosForRun(session, runId, items, { clearArchive: true });
|
|
21964
|
+
notifyTodosChanged();
|
|
21965
|
+
const daemon = daemonRef.current;
|
|
21966
|
+
if (!daemon) return;
|
|
21967
|
+
daemon.setTodos(items).then(async (applied) => {
|
|
21968
|
+
if (applied) return;
|
|
21969
|
+
await session.save();
|
|
21970
|
+
notifyTodosChanged();
|
|
21971
|
+
}).catch(() => {
|
|
21972
|
+
streamRef.current?.appendImmediate({
|
|
21973
|
+
kind: "info",
|
|
21974
|
+
text: "⚠ todo edit did not reach the daemon — it will re-sync on reconnect."
|
|
21975
|
+
});
|
|
21976
|
+
});
|
|
21977
|
+
}, [notifyTodosChanged]);
|
|
21978
|
+
useEffect(() => {
|
|
21979
|
+
if (!busy) return;
|
|
21980
|
+
let lastBeat = Promise.resolve();
|
|
21981
|
+
let beatSessionId = null;
|
|
21982
|
+
const timer = setInterval(() => {
|
|
21983
|
+
const agent = agentRef.current;
|
|
21984
|
+
const session = sessionRef.current;
|
|
21985
|
+
if (!settingsRef.current.useDaemon || isDaemonDisabledByEnv()) return;
|
|
21986
|
+
if (agent?.isRunning && session) {
|
|
21987
|
+
beatSessionId = session.id;
|
|
21988
|
+
lastBeat = store.updateStatus(session.id, "running").catch(() => void 0);
|
|
21989
|
+
}
|
|
21990
|
+
}, 6e4);
|
|
21991
|
+
return () => {
|
|
21992
|
+
clearInterval(timer);
|
|
21993
|
+
lastBeat.then(() => {
|
|
21994
|
+
const session = sessionRef.current;
|
|
21995
|
+
if (!beatSessionId || agentRef.current?.isRunning) return;
|
|
21996
|
+
const settled = session?.id === beatSessionId ? session.status : "idle";
|
|
21997
|
+
if (settled !== "running") store.updateStatus(beatSessionId, settled).catch(() => void 0);
|
|
21998
|
+
});
|
|
21999
|
+
};
|
|
22000
|
+
}, [busy, store]);
|
|
21453
22001
|
useEffect(() => attachTuiRendererState(() => sessionRef.current ?? void 0), []);
|
|
21454
22002
|
const chatReferenceScopeRef = useRef(chatReferenceScope);
|
|
21455
22003
|
chatReferenceScopeRef.current = chatReferenceScope;
|
|
@@ -21789,6 +22337,7 @@ function AppShell() {
|
|
|
21789
22337
|
setQueueSelectionIndex(null);
|
|
21790
22338
|
suppressWakeRef.current = true;
|
|
21791
22339
|
agentRef.current?.abort();
|
|
22340
|
+
daemonRef.current?.interrupt();
|
|
21792
22341
|
}, [denyAll, interactions]);
|
|
21793
22342
|
/**
|
|
21794
22343
|
* `ProviderKey` → runtime `Provider.name` for the providers constructed
|
|
@@ -22347,6 +22896,16 @@ function AppShell() {
|
|
|
22347
22896
|
});
|
|
22348
22897
|
agent.hooks.hook("agent:done", () => {
|
|
22349
22898
|
guardNoticed.clear();
|
|
22899
|
+
const record = [...session.runs].reverse().find((r) => r.parentRunId === void 0);
|
|
22900
|
+
if (!record) return;
|
|
22901
|
+
const receipt = runReceiptFromRun(record);
|
|
22902
|
+
const lastTurnId = session.turns.at(-1)?.id;
|
|
22903
|
+
if (hasRunReceipt(receipt)) stream.appendImmediate({
|
|
22904
|
+
kind: "run-summary",
|
|
22905
|
+
text: formatRunReceiptLine(receipt),
|
|
22906
|
+
runSummary: receipt,
|
|
22907
|
+
...lastTurnId ? { turnId: lastTurnId } : {}
|
|
22908
|
+
});
|
|
22350
22909
|
});
|
|
22351
22910
|
agent.hooks.hook("tool:before", async ({ callId, name, input, turnId }) => {
|
|
22352
22911
|
registerInFlightTool({
|
|
@@ -22381,6 +22940,7 @@ function AppShell() {
|
|
|
22381
22940
|
callId,
|
|
22382
22941
|
turnId
|
|
22383
22942
|
});
|
|
22943
|
+
if (name === "todowrite") notifyTodosChanged();
|
|
22384
22944
|
});
|
|
22385
22945
|
agent.hooks.hook("tool:error", ({ callId }) => {
|
|
22386
22946
|
unregisterInFlightTool(callId);
|
|
@@ -22491,18 +23051,8 @@ function AppShell() {
|
|
|
22491
23051
|
const reason = lastFinishReasonRef.current;
|
|
22492
23052
|
lastFinishReasonRef.current = void 0;
|
|
22493
23053
|
if (sessionRef.current?.id !== session.id) return;
|
|
22494
|
-
|
|
22495
|
-
|
|
22496
|
-
label: "output limit"
|
|
22497
|
-
}, "stopped at the model output limit (max_tokens). Turn on \"Auto-resume on output limit\" or raise \"Max output tokens\" in Settings to continue past it.");
|
|
22498
|
-
else if (reason === "pause") emitNotice({
|
|
22499
|
-
severity: "warn",
|
|
22500
|
-
label: "pause cap"
|
|
22501
|
-
}, "stopped after repeated empty pause turns from the provider (maxConsecutivePauseTurns cap is armed).");
|
|
22502
|
-
else if (reason === "content-filter") emitNotice({
|
|
22503
|
-
severity: "warn",
|
|
22504
|
-
label: "refusal"
|
|
22505
|
-
}, "the provider refused to continue (safety filter / refusal stop) despite automatic retries. Rephrase the request, or start a fresh session if it keeps happening.");
|
|
23054
|
+
const stop = runEndNotice(reason);
|
|
23055
|
+
if (stop) emitNotice(stop.notice, stop.text);
|
|
22506
23056
|
});
|
|
22507
23057
|
agent.hooks.hook("compact:start", ({ usedFraction }) => {
|
|
22508
23058
|
setCompacting(true);
|
|
@@ -22545,43 +23095,19 @@ function AppShell() {
|
|
|
22545
23095
|
}, `removed ${blocks} the provider rejected${ctx.persisted ? "" : " (session not yet persisted)"}`);
|
|
22546
23096
|
});
|
|
22547
23097
|
const spawnStartedAt = /* @__PURE__ */ new Map();
|
|
22548
|
-
agent.hooks.hook("spawn:before", (
|
|
23098
|
+
agent.hooks.hook("spawn:before", (ctx) => {
|
|
22549
23099
|
const startedAt = Date.now();
|
|
22550
|
-
|
|
22551
|
-
spawnStartedAt.set(id, {
|
|
23100
|
+
spawnStartedAt.set(ctx.id, {
|
|
22552
23101
|
startedAt,
|
|
22553
|
-
task:
|
|
22554
|
-
...model ? { model } : {}
|
|
22555
|
-
});
|
|
22556
|
-
stream.appendImmediate({
|
|
22557
|
-
kind: "spawn-start",
|
|
22558
|
-
text: resumesChildId ? `resumes ${resumesChildId} · ${taskPreview}` : taskPreview,
|
|
22559
|
-
childId: id,
|
|
22560
|
-
depth: depth ?? 1,
|
|
22561
|
-
spawn: { startedAt },
|
|
22562
|
-
...model ? { model } : {}
|
|
23102
|
+
task: previewLine(ctx.task, 80),
|
|
23103
|
+
...ctx.model ? { model: ctx.model } : {}
|
|
22563
23104
|
});
|
|
23105
|
+
stream.appendImmediate(buildSpawnStartEvent(ctx, startedAt));
|
|
22564
23106
|
});
|
|
22565
|
-
agent.hooks.hook("spawn:complete", (
|
|
22566
|
-
const
|
|
22567
|
-
|
|
22568
|
-
|
|
22569
|
-
spawnStartedAt.delete(id);
|
|
22570
|
-
const elapsed = startedAt !== void 0 ? ` · ${formatDuration(Date.now() - startedAt)}` : "";
|
|
22571
|
-
const cost = flattenTurns(stats).reduce((sum, turn) => sum + (turn.cost ?? 0), 0);
|
|
22572
|
-
const segments = `${elapsed}${cost > 0 ? ` · $${cost.toFixed(cost < .01 ? 4 : 2)}` : ""}`;
|
|
22573
|
-
stream.appendImmediate({
|
|
22574
|
-
kind: "spawn-end",
|
|
22575
|
-
text: segments ? `${tag}${segments} · ${formatTokenUsage(stats)}` : `${tag} ${formatTokenUsage(stats)}`,
|
|
22576
|
-
childId: id,
|
|
22577
|
-
depth: depth ?? 1,
|
|
22578
|
-
spawn: {
|
|
22579
|
-
startedAt: startedAt ?? Date.now(),
|
|
22580
|
-
...started?.task ? { task: started.task } : {},
|
|
22581
|
-
turns: stats.turns
|
|
22582
|
-
},
|
|
22583
|
-
...started?.model ? { model: started.model } : {}
|
|
22584
|
-
});
|
|
23107
|
+
agent.hooks.hook("spawn:complete", (ctx) => {
|
|
23108
|
+
const started = spawnStartedAt.get(ctx.id);
|
|
23109
|
+
spawnStartedAt.delete(ctx.id);
|
|
23110
|
+
stream.appendImmediate(buildSpawnEndEvent(ctx, started));
|
|
22585
23111
|
});
|
|
22586
23112
|
agent.hooks.hook("spawn:error", ({ id, depth, error }) => {
|
|
22587
23113
|
emitNotice({
|
|
@@ -22698,11 +23224,17 @@ function AppShell() {
|
|
|
22698
23224
|
unregisterInFlightTool,
|
|
22699
23225
|
extensionRegistry
|
|
22700
23226
|
]);
|
|
22701
|
-
const [
|
|
22702
|
-
const
|
|
23227
|
+
const [sessionsScopeOverride, setSessionsScopeOverride] = useState(null);
|
|
23228
|
+
const sessionsScope = sessionsScopeOverride ?? (settings.showAllProjects ? "all" : "project");
|
|
23229
|
+
const sessionsShowAll = sessionsScope !== "project";
|
|
22703
23230
|
const toggleSessionsScope = useCallback(() => {
|
|
22704
|
-
|
|
22705
|
-
|
|
23231
|
+
setSessionsScopeOverride((prev) => {
|
|
23232
|
+
const current = prev ?? (settingsRef.current.showAllProjects ? "all" : "project");
|
|
23233
|
+
if (current === "project") return "all";
|
|
23234
|
+
if (current === "all") return settingsRef.current.useDaemon && !isDaemonDisabledByEnv() ? "active" : "project";
|
|
23235
|
+
return "project";
|
|
23236
|
+
});
|
|
23237
|
+
}, []);
|
|
22706
23238
|
/** Debounced, cached full-history search for the conversations list. */
|
|
22707
23239
|
const onSessionSearchQueryChange = useCallback((raw) => {
|
|
22708
23240
|
sessionSearchQueryRef.current = raw;
|
|
@@ -22729,6 +23261,17 @@ function AppShell() {
|
|
|
22729
23261
|
});
|
|
22730
23262
|
sessionSearchTimerRef.current = setTimeout(() => {
|
|
22731
23263
|
(async () => {
|
|
23264
|
+
const remote = await daemonRef.current?.searchHistory(query, sessions.map((meta) => meta.id), 3);
|
|
23265
|
+
if (remote) {
|
|
23266
|
+
if (generation !== sessionSearchGenerationRef.current) return;
|
|
23267
|
+
setSessionContentMatches(new Map(Object.entries(remote)));
|
|
23268
|
+
setSessionContentSearchProgress({
|
|
23269
|
+
done: sessions.length,
|
|
23270
|
+
total: sessions.length
|
|
23271
|
+
});
|
|
23272
|
+
setSessionContentSearching(false);
|
|
23273
|
+
return;
|
|
23274
|
+
}
|
|
22732
23275
|
const matches = /* @__PURE__ */ new Map();
|
|
22733
23276
|
let cursor = 0;
|
|
22734
23277
|
let done = 0;
|
|
@@ -22839,18 +23382,112 @@ function AppShell() {
|
|
|
22839
23382
|
sessionSearchAbortRef.current?.abort();
|
|
22840
23383
|
historySearchWorkerRef.current?.terminate();
|
|
22841
23384
|
}, []);
|
|
23385
|
+
/**
|
|
23386
|
+
* Resolved "Main" orchestration session id. Learned from
|
|
23387
|
+
* `ensureMainSession` — the on-enable effect and the `openMainSession`
|
|
23388
|
+
* chord — so it is set as soon as the setting is on, and `null` while the
|
|
23389
|
+
* feature is off. What is deliberately NOT done is resolving it because the
|
|
23390
|
+
* sessions list was merely opened: that path creates the session lazily, and
|
|
23391
|
+
* browsing must not conjure one. The list only pins a row for an id it can
|
|
23392
|
+
* already find, so a stale/unknown value here is harmless.
|
|
23393
|
+
*/
|
|
23394
|
+
const [mainSessionId, setMainSessionId] = useState(null);
|
|
23395
|
+
/** In-flight local main creation — see `ensureMainSession`. */
|
|
23396
|
+
const mainSessionCreationRef = useRef(null);
|
|
23397
|
+
/**
|
|
23398
|
+
* Cache for the pin lookup below. Discovering the main costs an UNFILTERED
|
|
23399
|
+
* list (it is directory-less, so no scoped query returns it); the id is
|
|
23400
|
+
* stable for the store's lifetime, so pay that once and read one row by id
|
|
23401
|
+
* on every refresh after.
|
|
23402
|
+
*/
|
|
23403
|
+
const pinnedMainIdRef = useRef(null);
|
|
23404
|
+
/**
|
|
23405
|
+
* The main's row for the pin, or `null` when the store has no main yet.
|
|
23406
|
+
* Re-resolves when the cached id no longer names a main (deleted, or a
|
|
23407
|
+
* different store).
|
|
23408
|
+
*/
|
|
23409
|
+
const pinnedMainMeta = useCallback(async () => {
|
|
23410
|
+
const cached = pinnedMainIdRef.current;
|
|
23411
|
+
if (cached) {
|
|
23412
|
+
const meta = await getSessionMeta(store, cached);
|
|
23413
|
+
if (meta?.isMain) return meta;
|
|
23414
|
+
pinnedMainIdRef.current = null;
|
|
23415
|
+
}
|
|
23416
|
+
const found = (await listSessionMeta(store)).find((meta) => meta.isMain) ?? null;
|
|
23417
|
+
pinnedMainIdRef.current = found?.id ?? null;
|
|
23418
|
+
return found;
|
|
23419
|
+
}, [store]);
|
|
22842
23420
|
const refreshSessions = useCallback(async () => {
|
|
22843
|
-
const
|
|
23421
|
+
const raw = await listSessionMeta(store, sessionsShowAll ? void 0 : { projectRoot: projectDir });
|
|
23422
|
+
publishSessionRefs(raw);
|
|
23423
|
+
const list = settings.orchestration ? raw : raw.filter((meta) => !meta.isMain);
|
|
23424
|
+
if (settings.orchestration && !sessionsShowAll && !list.some((meta) => meta.isMain)) {
|
|
23425
|
+
const main = await pinnedMainMeta();
|
|
23426
|
+
if (main) {
|
|
23427
|
+
const merged = [main, ...list];
|
|
23428
|
+
setSessions(merged);
|
|
23429
|
+
return merged;
|
|
23430
|
+
}
|
|
23431
|
+
}
|
|
22844
23432
|
setSessions(list);
|
|
22845
23433
|
return list;
|
|
22846
23434
|
}, [
|
|
22847
23435
|
store,
|
|
22848
23436
|
sessionsShowAll,
|
|
22849
|
-
projectDir
|
|
23437
|
+
projectDir,
|
|
23438
|
+
settings.orchestration,
|
|
23439
|
+
pinnedMainMeta
|
|
22850
23440
|
]);
|
|
22851
23441
|
useEffect(() => {
|
|
22852
23442
|
refreshSessions();
|
|
22853
23443
|
}, [refreshSessions]);
|
|
23444
|
+
useEffect(() => {
|
|
23445
|
+
setSessionRefResolver(async (id) => {
|
|
23446
|
+
const meta = await getSessionMeta(store, id);
|
|
23447
|
+
if (!meta) return void 0;
|
|
23448
|
+
const dir = sessionDirLabel(meta.projectRoot);
|
|
23449
|
+
return dir ? {
|
|
23450
|
+
title: meta.title,
|
|
23451
|
+
dir
|
|
23452
|
+
} : { title: meta.title };
|
|
23453
|
+
});
|
|
23454
|
+
return () => setSessionRefResolver(null);
|
|
23455
|
+
}, [store]);
|
|
23456
|
+
const ensureMainSession = useCallback(async () => {
|
|
23457
|
+
if (!settings.orchestration) return null;
|
|
23458
|
+
const daemon = daemonRef.current;
|
|
23459
|
+
if (daemon) {
|
|
23460
|
+
const id = await daemon.mainSession();
|
|
23461
|
+
if (id) pinnedMainIdRef.current = id;
|
|
23462
|
+
return id;
|
|
23463
|
+
}
|
|
23464
|
+
if (mainSessionCreationRef.current) return mainSessionCreationRef.current;
|
|
23465
|
+
const inflight = (async () => {
|
|
23466
|
+
const existing = await pinnedMainMeta();
|
|
23467
|
+
if (existing) return existing.id;
|
|
23468
|
+
const id = await createMainSession(store);
|
|
23469
|
+
pinnedMainIdRef.current = id;
|
|
23470
|
+
await refreshSessions();
|
|
23471
|
+
return id;
|
|
23472
|
+
})();
|
|
23473
|
+
mainSessionCreationRef.current = inflight;
|
|
23474
|
+
try {
|
|
23475
|
+
return await inflight;
|
|
23476
|
+
} finally {
|
|
23477
|
+
mainSessionCreationRef.current = null;
|
|
23478
|
+
}
|
|
23479
|
+
}, [
|
|
23480
|
+
settings.orchestration,
|
|
23481
|
+
store,
|
|
23482
|
+
refreshSessions,
|
|
23483
|
+
pinnedMainMeta
|
|
23484
|
+
]);
|
|
23485
|
+
useEffect(() => {
|
|
23486
|
+
if (!settings.orchestration) return;
|
|
23487
|
+
ensureMainSession().then((id) => {
|
|
23488
|
+
if (id) setMainSessionId(id);
|
|
23489
|
+
}).catch((err) => debugLog("ensureMainSession failed", err));
|
|
23490
|
+
}, [settings.orchestration, ensureMainSession]);
|
|
22854
23491
|
/**
|
|
22855
23492
|
* Ref mirror for the resume-on-launch effect below: its calls must see
|
|
22856
23493
|
* the live callback WITHOUT depending on its identity — `refreshSessions`
|
|
@@ -22862,6 +23499,27 @@ function AppShell() {
|
|
|
22862
23499
|
const refreshSessionsRef = useRef(refreshSessions);
|
|
22863
23500
|
refreshSessionsRef.current = refreshSessions;
|
|
22864
23501
|
const teardown = useCallback(async () => {
|
|
23502
|
+
try {
|
|
23503
|
+
daemonRef.current?.dispose();
|
|
23504
|
+
} catch (err) {
|
|
23505
|
+
debugLog("teardown: daemon dispose failed", err);
|
|
23506
|
+
}
|
|
23507
|
+
daemonRef.current = null;
|
|
23508
|
+
setDaemonConnected(false);
|
|
23509
|
+
activationGenerationRef.current += 1;
|
|
23510
|
+
setRemoteDraft(null);
|
|
23511
|
+
setRemoteDraftAttachments(null);
|
|
23512
|
+
setRemoteSuggest(null);
|
|
23513
|
+
setRemoteSearchQuery(null);
|
|
23514
|
+
setRemoteHudState(null);
|
|
23515
|
+
setTranscriptSearchMode((prev) => {
|
|
23516
|
+
if (prev) searchPublishInitRef.current = true;
|
|
23517
|
+
return false;
|
|
23518
|
+
});
|
|
23519
|
+
setTranscriptSearchQuery((prev) => {
|
|
23520
|
+
if (prev.length > 0) searchPublishInitRef.current = true;
|
|
23521
|
+
return "";
|
|
23522
|
+
});
|
|
22865
23523
|
try {
|
|
22866
23524
|
denyAll();
|
|
22867
23525
|
} catch (err) {
|
|
@@ -23015,20 +23673,388 @@ function AppShell() {
|
|
|
23015
23673
|
}, [stream, autoCompactBehaviorFor]);
|
|
23016
23674
|
const activateSession = useCallback(async (id, key, opts) => {
|
|
23017
23675
|
await teardown();
|
|
23018
|
-
|
|
23676
|
+
activationGenerationRef.current += 1;
|
|
23677
|
+
const activationGeneration = activationGenerationRef.current;
|
|
23678
|
+
const loaded = id ? await loadSession(store, id) : null;
|
|
23679
|
+
const session = loaded ?? await createSession({
|
|
23019
23680
|
store,
|
|
23020
23681
|
projectRoot: projectDir,
|
|
23682
|
+
agentId: pickedAgentRef.current.id,
|
|
23021
23683
|
...id ? { id } : {},
|
|
23022
23684
|
...opts?.metadata ? { metadata: opts.metadata } : {}
|
|
23023
23685
|
});
|
|
23024
23686
|
sessionRef.current = session;
|
|
23025
|
-
|
|
23687
|
+
let stampedLegacyAgent = false;
|
|
23688
|
+
if (loaded) {
|
|
23689
|
+
const persistedProfile = session.agentId ? liveAgentRegistryRef.current[session.agentId] : void 0;
|
|
23690
|
+
if (persistedProfile && persistedProfile.id !== pickedAgentRef.current.id) {
|
|
23691
|
+
pickedAgentRef.current = persistedProfile;
|
|
23692
|
+
setPickedAgent(persistedProfile);
|
|
23693
|
+
} else if (!session.agentId) {
|
|
23694
|
+
session.setAgentId(pickedAgentRef.current.id);
|
|
23695
|
+
stampedLegacyAgent = true;
|
|
23696
|
+
}
|
|
23697
|
+
}
|
|
23698
|
+
if (settingsRef.current.useDaemon && !isDaemonDisabledByEnv()) try {
|
|
23699
|
+
agentRef.current = null;
|
|
23700
|
+
if (!loaded && typeof session.metadata.cwd !== "string") {
|
|
23701
|
+
session.setMeta("cwd", cwdRef.current ?? process.cwd());
|
|
23702
|
+
await session.save();
|
|
23703
|
+
}
|
|
23704
|
+
const activePick = pickedRef.current;
|
|
23705
|
+
let seedModelViaRpc = false;
|
|
23706
|
+
if (activePick) {
|
|
23707
|
+
if (typeof session.metadata.providerKey !== "string") if (loaded) seedModelViaRpc = true;
|
|
23708
|
+
else {
|
|
23709
|
+
session.setMeta("providerKey", activePick.provider.key);
|
|
23710
|
+
session.setMeta("modelId", activePick.model);
|
|
23711
|
+
if (activePick.effort) session.setMeta("effort", activePick.effort);
|
|
23712
|
+
if (activePick.modelOptions) session.setMeta("modelOptions", activePick.modelOptions);
|
|
23713
|
+
await session.save();
|
|
23714
|
+
}
|
|
23715
|
+
lastPushedModelRef.current = JSON.stringify([
|
|
23716
|
+
activePick.provider.key,
|
|
23717
|
+
activePick.model,
|
|
23718
|
+
activePick.effort ?? null,
|
|
23719
|
+
activePick.modelOptions ?? null
|
|
23720
|
+
]);
|
|
23721
|
+
} else lastPushedModelRef.current = null;
|
|
23722
|
+
const sessionId = session.id;
|
|
23723
|
+
const refreshHeaderFromStore = () => {
|
|
23724
|
+
store.load(sessionId).then((data) => {
|
|
23725
|
+
if (!data || sessionRef.current?.id !== sessionId) return;
|
|
23726
|
+
setCurrentSession({
|
|
23727
|
+
id: data.id,
|
|
23728
|
+
title: deriveSessionTitle(data.turns, data.metadata),
|
|
23729
|
+
turnCount: data.turns.length,
|
|
23730
|
+
userMessageCount: data.turns.reduce((n, t) => t.role === "user" ? n + 1 : n, 0),
|
|
23731
|
+
runCount: data.runs.length,
|
|
23732
|
+
updatedAt: data.updatedAt,
|
|
23733
|
+
...data.projectRoot ? { projectRoot: data.projectRoot } : {}
|
|
23734
|
+
});
|
|
23735
|
+
}).catch(() => void 0);
|
|
23736
|
+
};
|
|
23737
|
+
let controllerHandle = null;
|
|
23738
|
+
const reflectRemoteModel = () => {
|
|
23739
|
+
store.load(sessionId).then((data) => {
|
|
23740
|
+
if (!data || sessionRef.current?.id !== sessionId) return;
|
|
23741
|
+
const providerKey = data.metadata.providerKey;
|
|
23742
|
+
const modelId = data.metadata.modelId;
|
|
23743
|
+
if (typeof providerKey !== "string" || typeof modelId !== "string") return;
|
|
23744
|
+
const auth = availableProvidersRef.current.find((p) => p.key === providerKey);
|
|
23745
|
+
if (!auth) return;
|
|
23746
|
+
const next = makePicked(auth, modelId);
|
|
23747
|
+
if (!next) return;
|
|
23748
|
+
const effort = data.metadata.effort;
|
|
23749
|
+
const modelOptions = data.metadata.modelOptions;
|
|
23750
|
+
const merged = {
|
|
23751
|
+
...next,
|
|
23752
|
+
...typeof effort === "string" ? { effort } : {},
|
|
23753
|
+
...modelOptions && typeof modelOptions === "object" ? { modelOptions } : {}
|
|
23754
|
+
};
|
|
23755
|
+
const tuple = JSON.stringify([
|
|
23756
|
+
merged.provider.key,
|
|
23757
|
+
merged.model,
|
|
23758
|
+
merged.effort ?? null,
|
|
23759
|
+
merged.modelOptions ?? null
|
|
23760
|
+
]);
|
|
23761
|
+
if (lastPushedModelRef.current === tuple) return;
|
|
23762
|
+
lastPushedModelRef.current = tuple;
|
|
23763
|
+
setPicked(merged);
|
|
23764
|
+
}).catch(() => void 0);
|
|
23765
|
+
};
|
|
23766
|
+
const reflectRemoteAgent = () => {
|
|
23767
|
+
(store.getMeta ? store.getMeta(sessionId) : store.load(sessionId)).then((meta) => {
|
|
23768
|
+
if (!meta || sessionRef.current?.id !== sessionId) return;
|
|
23769
|
+
const remote = meta.agentId;
|
|
23770
|
+
if (!remote || remote === pickedAgentRef.current.id) return;
|
|
23771
|
+
sessionRef.current.setAgentId(remote);
|
|
23772
|
+
const profile = liveAgentRegistryRef.current[remote];
|
|
23773
|
+
if (!profile) return;
|
|
23774
|
+
pickedAgentRef.current = profile;
|
|
23775
|
+
setPickedAgent(profile);
|
|
23776
|
+
}).catch(() => void 0);
|
|
23777
|
+
};
|
|
23778
|
+
const resyncTranscript = () => {
|
|
23779
|
+
controllerHandle?.fetchTranscript().then(async (canonical) => {
|
|
23780
|
+
if (!canonical || sessionRef.current?.id !== sessionId || runningRef.current) return;
|
|
23781
|
+
await streamRef.current?.whenDrained();
|
|
23782
|
+
if (sessionRef.current?.id !== sessionId || runningRef.current) return;
|
|
23783
|
+
streamRef.current?.reset();
|
|
23784
|
+
setEvents(capTranscriptEvents(canonical));
|
|
23785
|
+
}).catch(() => void 0);
|
|
23786
|
+
};
|
|
23787
|
+
const controller = await connectDaemonSession({
|
|
23788
|
+
sessionId,
|
|
23789
|
+
appVersion: "tui",
|
|
23790
|
+
callbacks: {
|
|
23791
|
+
onRetry: ({ attempt, max, source }) => setRetryInfo({
|
|
23792
|
+
attempt,
|
|
23793
|
+
max,
|
|
23794
|
+
source
|
|
23795
|
+
}),
|
|
23796
|
+
onRetryCleared: (source) => clearRetryInfo(source),
|
|
23797
|
+
onPromptTurns: (refs) => {
|
|
23798
|
+
stream.flushAndUpdate((events) => retagUserPromptEventsFromRefs(events, refs));
|
|
23799
|
+
},
|
|
23800
|
+
onCompactionProgress: (tokens) => setCompactionTokens(tokens),
|
|
23801
|
+
onModelFallback: (from, to) => {
|
|
23802
|
+
setActiveFallback({
|
|
23803
|
+
provider: to.provider,
|
|
23804
|
+
model: to.model
|
|
23805
|
+
});
|
|
23806
|
+
stream.appendImmediate({
|
|
23807
|
+
kind: "info",
|
|
23808
|
+
text: `⚠ model fallback: ${from.provider}/${from.model} failed — continuing on ${to.provider}/${to.model}`
|
|
23809
|
+
});
|
|
23810
|
+
},
|
|
23811
|
+
onCacheBreak: (info) => {
|
|
23812
|
+
const providerKey = pickedRef.current?.provider.key;
|
|
23813
|
+
const apply = (costUsd) => {
|
|
23814
|
+
setActiveCacheBreak(costUsd !== void 0 ? {
|
|
23815
|
+
...info,
|
|
23816
|
+
costUsd
|
|
23817
|
+
} : info);
|
|
23818
|
+
};
|
|
23819
|
+
if (!providerKey) {
|
|
23820
|
+
apply(void 0);
|
|
23821
|
+
return;
|
|
23822
|
+
}
|
|
23823
|
+
estimateRebilledCostUsd(providerKey, info.model, info.rebilledTokens).then(apply).catch(() => apply(void 0));
|
|
23824
|
+
},
|
|
23825
|
+
onSystemMd: (file) => {
|
|
23826
|
+
sessionSystemFileRef.current = file;
|
|
23827
|
+
announceProjectSystemOverride();
|
|
23828
|
+
},
|
|
23829
|
+
onSettingsChanged: (remote) => {
|
|
23830
|
+
const local = settingsRef.current;
|
|
23831
|
+
const entries = Object.entries(remote).filter(([key, value]) => key in DEFAULT_SETTINGS && JSON.stringify(local[key]) !== JSON.stringify(value));
|
|
23832
|
+
if (entries.length === 0) return;
|
|
23833
|
+
suppressSettingsPublishRef.current = true;
|
|
23834
|
+
for (const [key, value] of entries) setSetting(key, value);
|
|
23835
|
+
},
|
|
23836
|
+
onSessionsChanged: (op, changedId) => {
|
|
23837
|
+
if (op === "update" && changedId === sessionId) {
|
|
23838
|
+
refreshHeaderFromStore();
|
|
23839
|
+
resyncTranscript();
|
|
23840
|
+
reflectRemoteModel();
|
|
23841
|
+
reflectRemoteAgent();
|
|
23842
|
+
}
|
|
23843
|
+
},
|
|
23844
|
+
onDraftChanged: (text, shell) => {
|
|
23845
|
+
draftSeqRef.current += 1;
|
|
23846
|
+
setRemoteDraft({
|
|
23847
|
+
seq: draftSeqRef.current,
|
|
23848
|
+
text,
|
|
23849
|
+
...shell ? { shell: true } : {}
|
|
23850
|
+
});
|
|
23851
|
+
},
|
|
23852
|
+
onSuggestChanged: ({ basis, caret, suggestion, candidates }) => {
|
|
23853
|
+
remoteSuggestSeqRef.current += 1;
|
|
23854
|
+
setRemoteSuggest({
|
|
23855
|
+
seq: remoteSuggestSeqRef.current,
|
|
23856
|
+
basis,
|
|
23857
|
+
caret,
|
|
23858
|
+
suggestion,
|
|
23859
|
+
...candidates ? { candidates } : {}
|
|
23860
|
+
});
|
|
23861
|
+
},
|
|
23862
|
+
onSearchChanged: ({ active, query }) => {
|
|
23863
|
+
setTranscriptSearchMode(active);
|
|
23864
|
+
setTranscriptSearchQuery(query);
|
|
23865
|
+
if (!active) setSelectedTurnId(null);
|
|
23866
|
+
remoteSearchSeqRef.current += 1;
|
|
23867
|
+
setRemoteSearchQuery(active ? {
|
|
23868
|
+
seq: remoteSearchSeqRef.current,
|
|
23869
|
+
query
|
|
23870
|
+
} : null);
|
|
23871
|
+
},
|
|
23872
|
+
onDraftAttachmentsChanged: (attachments) => {
|
|
23873
|
+
draftAttachmentsSeqRef.current += 1;
|
|
23874
|
+
setRemoteDraftAttachments({
|
|
23875
|
+
seq: draftAttachmentsSeqRef.current,
|
|
23876
|
+
attachments: attachments.map((att) => ({
|
|
23877
|
+
name: att.name ?? "attachment",
|
|
23878
|
+
mediaType: att.mediaType,
|
|
23879
|
+
content: Buffer.from(att.data, "base64")
|
|
23880
|
+
}))
|
|
23881
|
+
});
|
|
23882
|
+
},
|
|
23883
|
+
onSkillsChanged: (active) => {
|
|
23884
|
+
setActiveSkillNames(new Set(active));
|
|
23885
|
+
},
|
|
23886
|
+
onMcpAuth: ({ name, reason }) => {
|
|
23887
|
+
dispatchAuthRef.current({
|
|
23888
|
+
type: "auth-required",
|
|
23889
|
+
name,
|
|
23890
|
+
reason: reason === "auto-promoted" ? "auto-promoted" : "no-tokens"
|
|
23891
|
+
});
|
|
23892
|
+
},
|
|
23893
|
+
onHudState: (state) => {
|
|
23894
|
+
setRemoteHudState(state);
|
|
23895
|
+
},
|
|
23896
|
+
onHudNotify: (n) => {
|
|
23897
|
+
const allow = enabledExtensionsRef.current;
|
|
23898
|
+
if (allow !== void 0 && !allow.includes(n.extension)) return;
|
|
23899
|
+
stream.appendImmediate({
|
|
23900
|
+
kind: n.tone === "error" ? "error" : "info",
|
|
23901
|
+
text: `${n.extension}: ${n.tone === "warn" ? "⚠ " : ""}${n.text}`
|
|
23902
|
+
});
|
|
23903
|
+
},
|
|
23904
|
+
onExtensionSignal: (envelope) => {
|
|
23905
|
+
getTuiBus()?.deliver(envelope);
|
|
23906
|
+
},
|
|
23907
|
+
onTurnEnd: ({ usage, childId }) => {
|
|
23908
|
+
if (childId !== void 0) stream.flushAndUpdate((prev) => finalizeStreamingMarkdownForOwner(prev, childId));
|
|
23909
|
+
if (!usage) return;
|
|
23910
|
+
if (childId === void 0) {
|
|
23911
|
+
lastFinishReasonRef.current = usage.finishReason;
|
|
23912
|
+
const tokens = turnContextSize(usage);
|
|
23913
|
+
if (tokens > 0) {
|
|
23914
|
+
setLastInputTokens(tokens);
|
|
23915
|
+
lastInputTokensRef.current = tokens;
|
|
23916
|
+
}
|
|
23917
|
+
}
|
|
23918
|
+
const turnCost = usage.cost ?? 0;
|
|
23919
|
+
if (turnCost > 0) setSessionCost((prev) => prev + turnCost);
|
|
23920
|
+
},
|
|
23921
|
+
onTodosChanged: (snapshot) => {
|
|
23922
|
+
todosApplySeqRef.current += 1;
|
|
23923
|
+
const seq = todosApplySeqRef.current;
|
|
23924
|
+
const open = sessionRef.current;
|
|
23925
|
+
if (open?.id === sessionId && pickActiveRunId(open) === snapshot.runId) {
|
|
23926
|
+
applyTodosSnapshot(open, snapshot);
|
|
23927
|
+
notifyTodosChanged();
|
|
23928
|
+
return;
|
|
23929
|
+
}
|
|
23930
|
+
loadSession(store, sessionId).then((fresh) => {
|
|
23931
|
+
if (!fresh || sessionRef.current?.id !== sessionId) return;
|
|
23932
|
+
if (seq !== todosApplySeqRef.current) return;
|
|
23933
|
+
applyTodosSnapshot(fresh, snapshot);
|
|
23934
|
+
sessionRef.current = fresh;
|
|
23935
|
+
refreshHeaderFromStore();
|
|
23936
|
+
notifyTodosChanged();
|
|
23937
|
+
}).catch(() => void 0);
|
|
23938
|
+
},
|
|
23939
|
+
onWorkChanged: (items) => {
|
|
23940
|
+
const mapped = items.map((item) => ({
|
|
23941
|
+
kind: item.kind,
|
|
23942
|
+
callId: item.id,
|
|
23943
|
+
tool: item.label,
|
|
23944
|
+
startedAt: item.startedAt
|
|
23945
|
+
}));
|
|
23946
|
+
setInFlightTools(mapped.filter((m) => m.kind === "tool"));
|
|
23947
|
+
setBackgroundTasks(mapped.filter((m) => m.kind === "task"));
|
|
23948
|
+
},
|
|
23949
|
+
onQueueChanged: (items) => {
|
|
23950
|
+
daemonQueueIdsRef.current = items.map((item) => item.id);
|
|
23951
|
+
const mirrored = items.map((item) => ({
|
|
23952
|
+
prompt: item.text,
|
|
23953
|
+
references: [],
|
|
23954
|
+
attachments: []
|
|
23955
|
+
}));
|
|
23956
|
+
messageQueueRef.current = mirrored;
|
|
23957
|
+
setMessageQueue(mirrored);
|
|
23958
|
+
setQueueSelectionIndex((prev) => prev == null ? prev : mirrored.length === 0 ? null : Math.min(prev, mirrored.length - 1));
|
|
23959
|
+
},
|
|
23960
|
+
onInitialTranscript: (initial) => {
|
|
23961
|
+
setEvents(capTranscriptEvents(initial));
|
|
23962
|
+
},
|
|
23963
|
+
onStreamEvent: (event) => {
|
|
23964
|
+
const source = {
|
|
23965
|
+
...event.turnId ? { turnId: event.turnId } : {},
|
|
23966
|
+
...event.childId ? { childId: event.childId } : {},
|
|
23967
|
+
...typeof event.depth === "number" ? { depth: event.depth } : {}
|
|
23968
|
+
};
|
|
23969
|
+
if (event.kind === "markdown" && event.streaming) stream.queueStreamDelta("markdown", event.text, source);
|
|
23970
|
+
else if (event.kind === "thinking") stream.queueStreamDelta("thinking", event.text, source);
|
|
23971
|
+
else stream.appendImmediate(event);
|
|
23972
|
+
},
|
|
23973
|
+
onRunState: (running, info) => {
|
|
23974
|
+
runningRef.current = running;
|
|
23975
|
+
setBusy(running);
|
|
23976
|
+
if (running) return;
|
|
23977
|
+
stream.flushAndUpdate(finalizeStreamingMarkdown);
|
|
23978
|
+
if (info && !info.ok && info.error) stream.appendImmediate({
|
|
23979
|
+
kind: "error",
|
|
23980
|
+
text: info.error
|
|
23981
|
+
});
|
|
23982
|
+
refreshHeaderFromStore();
|
|
23983
|
+
resyncTranscript();
|
|
23984
|
+
},
|
|
23985
|
+
onApproval: (request, resolve) => {
|
|
23986
|
+
requestApproval(request.tool, request.input, request.originator, request.id).then(resolve);
|
|
23987
|
+
},
|
|
23988
|
+
onApprovalResolved: (requestId) => {
|
|
23989
|
+
dropApprovalByCorrelation(requestId);
|
|
23990
|
+
},
|
|
23991
|
+
onInteraction: (request, actions) => {
|
|
23992
|
+
interactions.enqueue({
|
|
23993
|
+
request,
|
|
23994
|
+
resolve: actions.respond,
|
|
23995
|
+
cancel: () => actions.cancel()
|
|
23996
|
+
});
|
|
23997
|
+
},
|
|
23998
|
+
onInteractionResolved: (requestId) => {
|
|
23999
|
+
interactions.dropById(requestId);
|
|
24000
|
+
},
|
|
24001
|
+
onCompacting: (active, info) => {
|
|
24002
|
+
setCompacting(active);
|
|
24003
|
+
if (!active && info?.ok && info.effectiveTokens !== void 0) {
|
|
24004
|
+
setLastInputTokens(info.effectiveTokens);
|
|
24005
|
+
lastInputTokensRef.current = info.effectiveTokens;
|
|
24006
|
+
}
|
|
24007
|
+
},
|
|
24008
|
+
onDisconnect: () => {
|
|
24009
|
+
runningRef.current = false;
|
|
24010
|
+
setBusy(false);
|
|
24011
|
+
setDaemonHealth("connecting");
|
|
24012
|
+
stream.appendImmediate({
|
|
24013
|
+
kind: "info",
|
|
24014
|
+
text: "daemon connection lost — reconnecting…"
|
|
24015
|
+
});
|
|
24016
|
+
setTimeout(() => {
|
|
24017
|
+
if (sessionRef.current?.id === sessionId && settingsRef.current.useDaemon && daemonRef.current === controllerHandle) activateSessionRef.current(sessionId, pickedRef.current?.provider.key ?? "anthropic").catch(() => {
|
|
24018
|
+
stream.appendImmediate({
|
|
24019
|
+
kind: "error",
|
|
24020
|
+
text: "reconnect failed — press ctrl+r or reopen the session to retry"
|
|
24021
|
+
});
|
|
24022
|
+
});
|
|
24023
|
+
}, 1500);
|
|
24024
|
+
}
|
|
24025
|
+
}
|
|
24026
|
+
});
|
|
24027
|
+
if (activationGenerationRef.current !== activationGeneration) {
|
|
24028
|
+
controller.dispose();
|
|
24029
|
+
return;
|
|
24030
|
+
}
|
|
24031
|
+
controllerHandle = controller;
|
|
24032
|
+
daemonRef.current = controller;
|
|
24033
|
+
setDaemonConnected(true);
|
|
24034
|
+
if (stampedLegacyAgent) controller.setAgentId(pickedAgentRef.current.id);
|
|
24035
|
+
if (seedModelViaRpc && activePick) controller.applyModel({
|
|
24036
|
+
providerKey: activePick.provider.key,
|
|
24037
|
+
modelId: activePick.model,
|
|
24038
|
+
...activePick.effort ? { effort: activePick.effort } : {},
|
|
24039
|
+
...activePick.modelOptions ? { modelOptions: activePick.modelOptions } : {}
|
|
24040
|
+
});
|
|
24041
|
+
} catch (err) {
|
|
24042
|
+
debugLog("daemon activation failed — falling back to in-process runtime", err);
|
|
24043
|
+
daemonRef.current = null;
|
|
24044
|
+
setDaemonConnected(false);
|
|
24045
|
+
agentRef.current = buildAgent(session, key);
|
|
24046
|
+
stream.appendImmediate({
|
|
24047
|
+
kind: "info",
|
|
24048
|
+
text: `daemon unavailable (${errorMessage(err)}) — running this session in-process. Toggle “Use daemon runtime” off to silence this.`
|
|
24049
|
+
});
|
|
24050
|
+
}
|
|
24051
|
+
else agentRef.current = buildAgent(session, key);
|
|
23026
24052
|
if (extensionRegistrySwapKeyRef.current === null || extensionRegistrySwapKeyRef.current === extensionRegistry) pendingAgentRebuildRef.current = false;
|
|
23027
24053
|
announcedSystemOverrideRef.current = null;
|
|
23028
24054
|
announceProjectSystemOverride();
|
|
23029
24055
|
const persistedPins = readPinnedSkills(session.metadata["zidane.activeSkills"]);
|
|
23030
24056
|
setActiveSkillNames(persistedPins);
|
|
23031
|
-
setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
|
|
24057
|
+
if (!daemonRef.current) setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
|
|
23032
24058
|
setActiveFallback(null);
|
|
23033
24059
|
const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
|
|
23034
24060
|
setLastInputTokens(replayedTokens);
|
|
@@ -23050,7 +24076,7 @@ function AppShell() {
|
|
|
23050
24076
|
lastProvider: key,
|
|
23051
24077
|
lastSessionId: session.id
|
|
23052
24078
|
});
|
|
23053
|
-
const pending = pendingInteractionsFromTurns(session.turns);
|
|
24079
|
+
const pending = daemonRef.current ? [] : pendingInteractionsFromTurns(session.turns);
|
|
23054
24080
|
if (pending.length > 0) {
|
|
23055
24081
|
const collected = /* @__PURE__ */ new Map();
|
|
23056
24082
|
const automatic = interactionModeRef.current === "skip";
|
|
@@ -23083,7 +24109,6 @@ function AppShell() {
|
|
|
23083
24109
|
extensionRegistry
|
|
23084
24110
|
]);
|
|
23085
24111
|
/** Ref mirror — same rationale as `refreshSessionsRef` above. */
|
|
23086
|
-
const activateSessionRef = useRef(activateSession);
|
|
23087
24112
|
activateSessionRef.current = activateSession;
|
|
23088
24113
|
useEffect(() => {
|
|
23089
24114
|
if (launch) return;
|
|
@@ -23124,6 +24149,7 @@ function AppShell() {
|
|
|
23124
24149
|
launch
|
|
23125
24150
|
]);
|
|
23126
24151
|
const [availableProviders, setAvailableProviders] = useState([]);
|
|
24152
|
+
availableProvidersRef.current = availableProviders;
|
|
23127
24153
|
const refreshAvailableProviders = useCallback(() => {
|
|
23128
24154
|
clearProviderUsageCache();
|
|
23129
24155
|
const detected = detectAuth(config.paths.userDir, providerRegistry);
|
|
@@ -23200,6 +24226,26 @@ function AppShell() {
|
|
|
23200
24226
|
setCwd,
|
|
23201
24227
|
activateSession
|
|
23202
24228
|
]);
|
|
24229
|
+
const orchestrationSeenRef = useRef(settings.orchestration);
|
|
24230
|
+
useEffect(() => {
|
|
24231
|
+
if (orchestrationSeenRef.current === settings.orchestration) return;
|
|
24232
|
+
orchestrationSeenRef.current = settings.orchestration;
|
|
24233
|
+
if (settings.orchestration) return;
|
|
24234
|
+
const current = sessionRef.current;
|
|
24235
|
+
if (!current || !isMainSession(current.metadata)) return;
|
|
24236
|
+
setMainSessionId(null);
|
|
24237
|
+
refreshSessions().then(() => setScreen("sessions")).catch((err) => debugLog("leave main: refresh failed", err));
|
|
24238
|
+
}, [settings.orchestration, refreshSessions]);
|
|
24239
|
+
const daemonHealth = useDaemonHealth();
|
|
24240
|
+
const useDaemonSeenRef = useRef(settings.useDaemon);
|
|
24241
|
+
useEffect(() => {
|
|
24242
|
+
if (useDaemonSeenRef.current === settings.useDaemon) return;
|
|
24243
|
+
useDaemonSeenRef.current = settings.useDaemon;
|
|
24244
|
+
const current = sessionRef.current;
|
|
24245
|
+
const key = pickedRef.current?.provider.key;
|
|
24246
|
+
if (!current || !key) return;
|
|
24247
|
+
activateSession(current.id, key).catch((err) => debugLog("runtime switch: re-activation failed", err));
|
|
24248
|
+
}, [settings.useDaemon, activateSession]);
|
|
23203
24249
|
useEffect(() => {
|
|
23204
24250
|
const pending = pendingSessionSwitchRef.current;
|
|
23205
24251
|
if (!pending || cwd !== pending.dir || !picked || busy) return;
|
|
@@ -23553,6 +24599,11 @@ function AppShell() {
|
|
|
23553
24599
|
return;
|
|
23554
24600
|
}
|
|
23555
24601
|
const dropIdx = Math.min(idx, queue.length - 1);
|
|
24602
|
+
if (daemonRef.current) {
|
|
24603
|
+
const id = daemonQueueIdsRef.current[dropIdx];
|
|
24604
|
+
if (id) daemonRef.current.queueRemove(id);
|
|
24605
|
+
return;
|
|
24606
|
+
}
|
|
23556
24607
|
messageQueueRef.current = queue.filter((_, i) => i !== dropIdx);
|
|
23557
24608
|
setMessageQueue(messageQueueRef.current.slice());
|
|
23558
24609
|
if (messageQueueRef.current.length === 0) setQueueSelectionIndex(null);
|
|
@@ -23571,10 +24622,15 @@ function AppShell() {
|
|
|
23571
24622
|
* it up as a regular prompt, so the message is never silently dropped.
|
|
23572
24623
|
*/
|
|
23573
24624
|
const pushSelectedQueuedMessage = useCallback(() => {
|
|
23574
|
-
const agent = agentRef.current;
|
|
23575
|
-
if (!agent) return;
|
|
23576
24625
|
const idx = queueSelectionIndexRef.current;
|
|
23577
24626
|
if (idx == null) return;
|
|
24627
|
+
if (daemonRef.current) {
|
|
24628
|
+
const pushId = daemonQueueIdsRef.current[Math.min(idx, daemonQueueIdsRef.current.length - 1)];
|
|
24629
|
+
if (pushId) daemonRef.current.queuePushNow(pushId);
|
|
24630
|
+
return;
|
|
24631
|
+
}
|
|
24632
|
+
const agent = agentRef.current;
|
|
24633
|
+
if (!agent) return;
|
|
23578
24634
|
const queue = messageQueueRef.current;
|
|
23579
24635
|
if (queue.length === 0) {
|
|
23580
24636
|
setQueueSelectionIndex(null);
|
|
@@ -23787,6 +24843,18 @@ function AppShell() {
|
|
|
23787
24843
|
lastAgent: id
|
|
23788
24844
|
});
|
|
23789
24845
|
modal.close();
|
|
24846
|
+
const session = sessionRef.current;
|
|
24847
|
+
if (daemonRef.current) {
|
|
24848
|
+
if (session && session.agentId !== id) {
|
|
24849
|
+
session.setAgentId(id);
|
|
24850
|
+
await daemonRef.current.setAgentId(id).catch((err) => debugLog("setAgentId over the wire failed", err));
|
|
24851
|
+
}
|
|
24852
|
+
return;
|
|
24853
|
+
}
|
|
24854
|
+
if (session && session.agentId !== id) {
|
|
24855
|
+
session.setAgentId(id);
|
|
24856
|
+
await session.save().catch(() => void 0);
|
|
24857
|
+
}
|
|
23790
24858
|
if (picked && currentSession && !busy) await activateSession(currentSession.id, picked.provider.key);
|
|
23791
24859
|
}, [
|
|
23792
24860
|
liveAgentRegistry,
|
|
@@ -23834,6 +24902,15 @@ function AppShell() {
|
|
|
23834
24902
|
const agent = agentRef.current;
|
|
23835
24903
|
const session = sessionRef.current;
|
|
23836
24904
|
if (!agent || !session || !picked) return;
|
|
24905
|
+
const persisted = settingsRef.current.useDaemon && !isDaemonDisabledByEnv() ? await (store.getMeta ? store.getMeta(session.id) : store.load(session.id)).catch(() => null) : null;
|
|
24906
|
+
const ownEnd = lastLocalRunEndRef.current;
|
|
24907
|
+
if (persisted?.status === "running" && Date.now() - persisted.updatedAt < 300 * 1e3 && !agent.isRunning && !(ownEnd?.id === session.id && persisted.updatedAt <= ownEnd.at + 2e3)) {
|
|
24908
|
+
stream.appendImmediate({
|
|
24909
|
+
kind: "error",
|
|
24910
|
+
text: "this session is running in another process (a daemon window?) — wait for it to finish, interrupt it there, or enable \"Use daemon runtime\" to share the run."
|
|
24911
|
+
});
|
|
24912
|
+
return;
|
|
24913
|
+
}
|
|
23837
24914
|
if (eventsLengthRef.current > 0) stream.appendImmediate({
|
|
23838
24915
|
kind: "separator",
|
|
23839
24916
|
text: ""
|
|
@@ -23932,6 +25009,10 @@ function AppShell() {
|
|
|
23932
25009
|
label: "run"
|
|
23933
25010
|
}, errorMessage(err), { session });
|
|
23934
25011
|
} finally {
|
|
25012
|
+
lastLocalRunEndRef.current = {
|
|
25013
|
+
id: session.id,
|
|
25014
|
+
at: Date.now()
|
|
25015
|
+
};
|
|
23935
25016
|
chatReferenceTokensRef.current = [];
|
|
23936
25017
|
stream.flushAndUpdate(finalizeStreamingMarkdown);
|
|
23937
25018
|
}
|
|
@@ -24302,6 +25383,7 @@ function AppShell() {
|
|
|
24302
25383
|
} finally {
|
|
24303
25384
|
commandAbortRef.current = null;
|
|
24304
25385
|
await flushCommandTurn();
|
|
25386
|
+
daemonRef.current?.touchSession();
|
|
24305
25387
|
setCurrentSession((prev) => prev ? {
|
|
24306
25388
|
...prev,
|
|
24307
25389
|
title: deriveSessionTitle(session.turns, session.metadata),
|
|
@@ -24417,6 +25499,7 @@ function AppShell() {
|
|
|
24417
25499
|
}, `!${command}: ${errorMessage(err)}`, { session });
|
|
24418
25500
|
} finally {
|
|
24419
25501
|
commandAbortRef.current = null;
|
|
25502
|
+
daemonRef.current?.touchSession();
|
|
24420
25503
|
setCurrentSession((prev) => prev ? {
|
|
24421
25504
|
...prev,
|
|
24422
25505
|
turnCount: session.turns.length,
|
|
@@ -24468,7 +25551,7 @@ function AppShell() {
|
|
|
24468
25551
|
await rebuildActiveAgentRef.current().catch((err) => debugLog("agent-rebuild: rebuildActiveAgent failed", err));
|
|
24469
25552
|
continue;
|
|
24470
25553
|
}
|
|
24471
|
-
if (messageQueueRef.current.length > 0) {
|
|
25554
|
+
if (!daemonRef.current && messageQueueRef.current.length > 0) {
|
|
24472
25555
|
const next = messageQueueRef.current[0];
|
|
24473
25556
|
messageQueueRef.current = messageQueueRef.current.slice(1);
|
|
24474
25557
|
setMessageQueue(messageQueueRef.current.slice());
|
|
@@ -24491,8 +25574,9 @@ function AppShell() {
|
|
|
24491
25574
|
const onSubmitPrompt = useCallback((prompt, references, attachments) => {
|
|
24492
25575
|
if (!prompt.trim() && attachments.length === 0) return;
|
|
24493
25576
|
const agent = agentRef.current;
|
|
25577
|
+
const daemon = daemonRef.current;
|
|
24494
25578
|
const session = sessionRef.current;
|
|
24495
|
-
if (!agent || !session || !picked) return;
|
|
25579
|
+
if (!agent && !daemon || !session || !picked) return;
|
|
24496
25580
|
const wasRunning = runningRef.current;
|
|
24497
25581
|
if (!wasRunning) runningRef.current = true;
|
|
24498
25582
|
transcriptRevealHoldRef.current = false;
|
|
@@ -24517,6 +25601,31 @@ function AppShell() {
|
|
|
24517
25601
|
debugLog(`prompt transform (${contribution.extensionName}) threw`, err);
|
|
24518
25602
|
}
|
|
24519
25603
|
const resolvedReferences = await resolveCompletionSubmit(effectivePrompt, completionProviders, { cursor: effectivePrompt.length }).catch(() => references);
|
|
25604
|
+
if (daemon) {
|
|
25605
|
+
if (!wasRunning) runningRef.current = false;
|
|
25606
|
+
if (parseCommandInvocation(effectivePrompt, commandsRef.current) !== null || parseBashInvocation(effectivePrompt) !== null) {
|
|
25607
|
+
daemon.publishDraft("");
|
|
25608
|
+
await runPromptOrCommand(effectivePrompt, resolvedReferences, attachments);
|
|
25609
|
+
return;
|
|
25610
|
+
}
|
|
25611
|
+
const skillNames = Array.from(/* @__PURE__ */ new Set([...activeSkillNamesRef.current, ...uniqueSkillNamesFromReferences(resolvedReferences)]));
|
|
25612
|
+
try {
|
|
25613
|
+
await daemon.queueAdd(effectivePrompt, {
|
|
25614
|
+
...skillNames.length > 0 ? { activatedSkills: [...skillNames] } : {},
|
|
25615
|
+
...attachments.length > 0 ? { attachments: attachments.map((a) => ({
|
|
25616
|
+
mediaType: a.mediaType,
|
|
25617
|
+
data: a.content.toString("base64"),
|
|
25618
|
+
name: a.name
|
|
25619
|
+
})) } : {}
|
|
25620
|
+
});
|
|
25621
|
+
} catch (err) {
|
|
25622
|
+
stream.appendImmediate({
|
|
25623
|
+
kind: "error",
|
|
25624
|
+
text: errorMessage(err)
|
|
25625
|
+
});
|
|
25626
|
+
}
|
|
25627
|
+
return;
|
|
25628
|
+
}
|
|
24520
25629
|
if (wasRunning) {
|
|
24521
25630
|
messageQueueRef.current = [...messageQueueRef.current, {
|
|
24522
25631
|
prompt: effectivePrompt,
|
|
@@ -24541,7 +25650,8 @@ function AppShell() {
|
|
|
24541
25650
|
runPromptOrCommand,
|
|
24542
25651
|
drainFollowUps,
|
|
24543
25652
|
completionProviders,
|
|
24544
|
-
extensionRegistry
|
|
25653
|
+
extensionRegistry,
|
|
25654
|
+
stream
|
|
24545
25655
|
]);
|
|
24546
25656
|
useEffect(() => {
|
|
24547
25657
|
const bridge = getTuiPromptBridge();
|
|
@@ -24709,6 +25819,10 @@ function AppShell() {
|
|
|
24709
25819
|
const session = sessionRef.current;
|
|
24710
25820
|
const p = pickedRef.current;
|
|
24711
25821
|
if (!session || !p) return;
|
|
25822
|
+
if (daemonRef.current) {
|
|
25823
|
+
await daemonRef.current.rebuildAgent();
|
|
25824
|
+
return;
|
|
25825
|
+
}
|
|
24712
25826
|
const stale = agentRef.current;
|
|
24713
25827
|
const staleWarnLog = warnLogRef.current;
|
|
24714
25828
|
const fresh = buildAgentRef.current(session, p.provider.key);
|
|
@@ -24745,7 +25859,7 @@ function AppShell() {
|
|
|
24745
25859
|
return (provider, source) => {
|
|
24746
25860
|
const descriptor = providerRegistry[provider.key];
|
|
24747
25861
|
if (!descriptor) return void 0;
|
|
24748
|
-
setPreferredCredential(config.paths.userDir, descriptor, source);
|
|
25862
|
+
if (setPreferredCredential(config.paths.userDir, descriptor, source) !== "promoted") return void 0;
|
|
24749
25863
|
applyApiKeyEnv(config.paths.userDir, providerRegistry, { log: (message) => getTuiLogging()?.sink.emit({
|
|
24750
25864
|
level: "debug",
|
|
24751
25865
|
timestamp: Date.now(),
|
|
@@ -25049,6 +26163,13 @@ function AppShell() {
|
|
|
25049
26163
|
transcriptSearchAbortRef.current = abortController;
|
|
25050
26164
|
setTranscriptSearchSearching(true);
|
|
25051
26165
|
(async () => {
|
|
26166
|
+
const remote = await daemonRef.current?.searchTranscript(query);
|
|
26167
|
+
if (remote) {
|
|
26168
|
+
if (generation !== transcriptSearchGenerationRef.current) return;
|
|
26169
|
+
setTranscriptSearchResults(remote);
|
|
26170
|
+
setTranscriptSearchSearching(false);
|
|
26171
|
+
return;
|
|
26172
|
+
}
|
|
25052
26173
|
const cached = transcriptSearchIndexRef.current;
|
|
25053
26174
|
const index = cached?.sessionId === session.id && cached.events === events ? cached.index : await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(turns, abortController.signal);
|
|
25054
26175
|
if (cached && cached.index !== index) historySearchWorkerRef.current?.release(cached.index);
|
|
@@ -25167,6 +26288,72 @@ function AppShell() {
|
|
|
25167
26288
|
const onTranscriptSearchNavigate = useCallback((direction) => {
|
|
25168
26289
|
cycleSelectedTurn(direction, transcriptSearchTurnIds);
|
|
25169
26290
|
}, [cycleSelectedTurn, transcriptSearchTurnIds]);
|
|
26291
|
+
/**
|
|
26292
|
+
* Daemon-mode preamble for HISTORY MUTATIONS (edit / delete). These
|
|
26293
|
+
* write the shared store through the window's load-time wrapper, which
|
|
26294
|
+
* LAGS the daemon's live appends — mutating it blind would clobber
|
|
26295
|
+
* turns other windows added since this one loaded. Refuses mid-run
|
|
26296
|
+
* (the daemon's agent is appending right now), refreshes the wrapper
|
|
26297
|
+
* to the store's current truth, and returns it. `null` = don't mutate.
|
|
26298
|
+
*/
|
|
26299
|
+
const prepareHistoryMutation = useCallback(async () => {
|
|
26300
|
+
const session = sessionRef.current;
|
|
26301
|
+
if (!session) return null;
|
|
26302
|
+
if (!daemonRef.current) return session;
|
|
26303
|
+
if (runningRef.current) {
|
|
26304
|
+
stream.appendImmediate({
|
|
26305
|
+
kind: "error",
|
|
26306
|
+
text: "history can't be edited while a run is in flight — interrupt it first."
|
|
26307
|
+
});
|
|
26308
|
+
return null;
|
|
26309
|
+
}
|
|
26310
|
+
const fresh = await loadSession(store, session.id).catch(() => null);
|
|
26311
|
+
if (!fresh) {
|
|
26312
|
+
stream.appendImmediate({
|
|
26313
|
+
kind: "error",
|
|
26314
|
+
text: "could not reload the session for editing — try again."
|
|
26315
|
+
});
|
|
26316
|
+
return null;
|
|
26317
|
+
}
|
|
26318
|
+
sessionRef.current = fresh;
|
|
26319
|
+
return fresh;
|
|
26320
|
+
}, [store, stream]);
|
|
26321
|
+
/**
|
|
26322
|
+
* Daemon-mode epilogue: the daemon's cached agent still holds the
|
|
26323
|
+
* PRE-EDIT history in memory — retire it so the next run reloads the
|
|
26324
|
+
* edited turns (and can't save the stale wrapper back over them) —
|
|
26325
|
+
* then announce the out-of-band store change so every window
|
|
26326
|
+
* re-converges now.
|
|
26327
|
+
*/
|
|
26328
|
+
/**
|
|
26329
|
+
* Guarded save for history mutations: the run check in
|
|
26330
|
+
* `prepareHistoryMutation` and this save are not atomic — a run
|
|
26331
|
+
* starting in between (another window's send, a queue drain, a wake)
|
|
26332
|
+
* appends turns this full-document save would erase. Re-check the
|
|
26333
|
+
* store's run marker IMMEDIATELY before writing and refuse the same
|
|
26334
|
+
* way the preamble does. Narrows the race to the store round-trip;
|
|
26335
|
+
* a store-level updatedAt CAS is the tracked full fix.
|
|
26336
|
+
*/
|
|
26337
|
+
const saveHistoryMutation = useCallback(async (session) => {
|
|
26338
|
+
if (daemonRef.current) {
|
|
26339
|
+
const meta = await (store.getMeta?.(session.id) ?? store.load(session.id)).catch(() => null);
|
|
26340
|
+
if (meta?.status === "running" && typeof meta.updatedAt === "number" && Date.now() - meta.updatedAt < 300 * 1e3) {
|
|
26341
|
+
stream.appendImmediate({
|
|
26342
|
+
kind: "error",
|
|
26343
|
+
text: "a run started while editing — history was not modified. Interrupt the run and retry."
|
|
26344
|
+
});
|
|
26345
|
+
return false;
|
|
26346
|
+
}
|
|
26347
|
+
}
|
|
26348
|
+
await session.save();
|
|
26349
|
+
return true;
|
|
26350
|
+
}, [store, stream]);
|
|
26351
|
+
const finishHistoryMutation = useCallback(async () => {
|
|
26352
|
+
const daemon = daemonRef.current;
|
|
26353
|
+
if (!daemon) return;
|
|
26354
|
+
await daemon.rebuildAgent();
|
|
26355
|
+
daemon.touchSession();
|
|
26356
|
+
}, []);
|
|
25170
26357
|
const onForkTurn = useCallback(async (turnId) => {
|
|
25171
26358
|
const source = sessionRef.current;
|
|
25172
26359
|
if (!source || !picked) return;
|
|
@@ -25177,6 +26364,7 @@ function AppShell() {
|
|
|
25177
26364
|
const inheritedRuns = source.runs.filter((r) => referencedRunIds.has(r.id)).map((r) => ({ ...r }));
|
|
25178
26365
|
const fork = await createSession({
|
|
25179
26366
|
store,
|
|
26367
|
+
agentId: source.agentId ?? pickedAgentRef.current.id,
|
|
25180
26368
|
...source.projectRoot ? { projectRoot: source.projectRoot } : {}
|
|
25181
26369
|
});
|
|
25182
26370
|
const forkPersistDir = resolvePersistDir({
|
|
@@ -25209,17 +26397,18 @@ function AppShell() {
|
|
|
25209
26397
|
cacheDir
|
|
25210
26398
|
]);
|
|
25211
26399
|
const onDeleteTurn = useCallback(async (turnId) => {
|
|
25212
|
-
const session =
|
|
26400
|
+
const session = await prepareHistoryMutation();
|
|
25213
26401
|
if (!session) return;
|
|
25214
26402
|
const nextTurns = deleteTurnSafely(session.turns, turnId);
|
|
25215
26403
|
if (!nextTurns) return;
|
|
25216
26404
|
session.setTurns(nextTurns);
|
|
25217
26405
|
try {
|
|
25218
|
-
await session
|
|
26406
|
+
if (!await saveHistoryMutation(session)) return;
|
|
25219
26407
|
} catch (err) {
|
|
25220
26408
|
debugLog("delete: save failed", err);
|
|
25221
26409
|
return;
|
|
25222
26410
|
}
|
|
26411
|
+
await finishHistoryMutation();
|
|
25223
26412
|
const nextEvents = capTranscriptEvents(eventsFromTurns(session.turns, session.runs));
|
|
25224
26413
|
const nextIds = selectableTurnIds(nextEvents, settings);
|
|
25225
26414
|
setEvents(nextEvents);
|
|
@@ -25237,7 +26426,7 @@ function AppShell() {
|
|
|
25237
26426
|
} : prev);
|
|
25238
26427
|
}, [settings, turnIds]);
|
|
25239
26428
|
const onEditTurn = useCallback(async (turnId, newText) => {
|
|
25240
|
-
const session =
|
|
26429
|
+
const session = await prepareHistoryMutation();
|
|
25241
26430
|
if (!session) return;
|
|
25242
26431
|
const turn = session.turns.find((t) => t.id === turnId);
|
|
25243
26432
|
if (!turn) return;
|
|
@@ -25252,11 +26441,12 @@ function AppShell() {
|
|
|
25252
26441
|
turn.content = updatedContent;
|
|
25253
26442
|
session.setTurns([...session.turns]);
|
|
25254
26443
|
try {
|
|
25255
|
-
await session
|
|
26444
|
+
if (!await saveHistoryMutation(session)) return;
|
|
25256
26445
|
} catch (err) {
|
|
25257
26446
|
debugLog("edit: save failed", err);
|
|
25258
26447
|
return;
|
|
25259
26448
|
}
|
|
26449
|
+
await finishHistoryMutation();
|
|
25260
26450
|
const rebuilt = eventsFromTurns(session.turns, session.runs);
|
|
25261
26451
|
const patched = replaceTranscriptTurnEvents(eventsRef.current, rebuilt, turnId);
|
|
25262
26452
|
setEvents(patched ?? capTranscriptEvents(rebuilt));
|
|
@@ -25342,6 +26532,7 @@ function AppShell() {
|
|
|
25342
26532
|
turns = loadedData.turns;
|
|
25343
26533
|
metadataRecord = loadedData.metadata;
|
|
25344
26534
|
}
|
|
26535
|
+
if (isMainSession(metadataRecord)) throw new Error("The Main session keeps its name.");
|
|
25345
26536
|
const title = await generateSessionTitle({
|
|
25346
26537
|
provider: descriptor.factory(),
|
|
25347
26538
|
model: picked.model,
|
|
@@ -25784,13 +26975,27 @@ function AppShell() {
|
|
|
25784
26975
|
}
|
|
25785
26976
|
if (matchesBinding(key, keybindings.openTodos) && screen === "chat" && currentSession) {
|
|
25786
26977
|
modal.open(/* @__PURE__ */ jsx$1(TodosModal, {
|
|
25787
|
-
|
|
25788
|
-
|
|
26978
|
+
getSession: () => sessionRef.current,
|
|
26979
|
+
subscribe: subscribeTodos,
|
|
26980
|
+
onCommit: commitTodos,
|
|
25789
26981
|
onSteer: (message) => onSubmitPrompt(message, [], [])
|
|
25790
26982
|
}));
|
|
25791
26983
|
return;
|
|
25792
26984
|
}
|
|
25793
26985
|
if (matchesBinding(key, keybindings.openContextPanel) && screen === "chat" && picked) {
|
|
26986
|
+
if (daemonRef.current) {
|
|
26987
|
+
const controller = daemonRef.current;
|
|
26988
|
+
controller.fetchContextBreakdown().then((breakdown) => {
|
|
26989
|
+
if (breakdown && daemonRef.current === controller) {
|
|
26990
|
+
const modelUsage = usageByModel(sessionRef.current?.runs ?? []);
|
|
26991
|
+
modal.open(/* @__PURE__ */ jsx$1(ContextPanelModal, {
|
|
26992
|
+
breakdown,
|
|
26993
|
+
modelUsage
|
|
26994
|
+
}));
|
|
26995
|
+
}
|
|
26996
|
+
}).catch((err) => debugLog("daemon contextBreakdown failed", err));
|
|
26997
|
+
return;
|
|
26998
|
+
}
|
|
25794
26999
|
const agent = agentRef.current;
|
|
25795
27000
|
if (!agent) return;
|
|
25796
27001
|
const descriptor = providerRegistry[picked.provider.key];
|
|
@@ -25811,6 +27016,15 @@ function AppShell() {
|
|
|
25811
27016
|
}).catch((err) => debugLog("getContextBreakdown failed", err));
|
|
25812
27017
|
return;
|
|
25813
27018
|
}
|
|
27019
|
+
if (matchesBinding(key, keybindings.openMainSession) && settings.orchestration) {
|
|
27020
|
+
ensureMainSession().then((mainId) => {
|
|
27021
|
+
if (mainId) {
|
|
27022
|
+
setMainSessionId(mainId);
|
|
27023
|
+
onSwitchSession(mainId);
|
|
27024
|
+
}
|
|
27025
|
+
}).catch((err) => debugLog("ensureMainSession failed", err));
|
|
27026
|
+
return;
|
|
27027
|
+
}
|
|
25814
27028
|
if (matchesBinding(key, keybindings.openUsagePanel) && screen === "chat") {
|
|
25815
27029
|
modal.open(/* @__PURE__ */ jsx$1(UsagePanelModal, {
|
|
25816
27030
|
dataDir: config.paths.userDir,
|
|
@@ -25846,12 +27060,17 @@ function AppShell() {
|
|
|
25846
27060
|
modal.open(/* @__PURE__ */ jsx$1(CancelToolModal, {
|
|
25847
27061
|
inFlight: snapshot,
|
|
25848
27062
|
onCancel: (entry, reason) => {
|
|
27063
|
+
if (daemonRef.current) return daemonRef.current.cancelWork(entry.callId, reason);
|
|
25849
27064
|
const agent = agentRef.current;
|
|
25850
27065
|
if (!agent) return false;
|
|
25851
27066
|
if (entry.kind === "task") return agent.killBackgroundTask(entry.callId);
|
|
25852
27067
|
return agent.cancelTool(entry.callId, reason);
|
|
25853
27068
|
},
|
|
25854
27069
|
onCancelAll: () => {
|
|
27070
|
+
if (daemonRef.current) {
|
|
27071
|
+
daemonRef.current.cancelWork("all");
|
|
27072
|
+
return;
|
|
27073
|
+
}
|
|
25855
27074
|
const agent = agentRef.current;
|
|
25856
27075
|
if (!agent) return;
|
|
25857
27076
|
for (const entry of snapshot) if (entry.kind === "task") agent.killBackgroundTask(entry.callId);
|
|
@@ -25964,19 +27183,13 @@ function AppShell() {
|
|
|
25964
27183
|
const updateHint = useMemo(() => buildUpdateHint(updateStatus, { color: COLOR.accent }), [updateStatus, COLOR.accent]);
|
|
25965
27184
|
const cacheBreakHint = useMemo(() => {
|
|
25966
27185
|
if (!activeCacheBreak) return null;
|
|
25967
|
-
const
|
|
25968
|
-
const
|
|
25969
|
-
if (activeCacheBreak.changed.length === 0) return {
|
|
25970
|
-
key: "⚡",
|
|
25971
|
-
keyColor: COLOR.dim,
|
|
25972
|
-
label: `cache expired · +${rebilled} tok${cost}`,
|
|
25973
|
-
labelColor: COLOR.dim
|
|
25974
|
-
};
|
|
27186
|
+
const chip = cacheBreakChip(activeCacheBreak);
|
|
27187
|
+
const color = chip.tone === "warn" ? COLOR.warn : COLOR.dim;
|
|
25975
27188
|
return {
|
|
25976
|
-
key:
|
|
25977
|
-
keyColor:
|
|
25978
|
-
label:
|
|
25979
|
-
labelColor:
|
|
27189
|
+
key: chip.glyph,
|
|
27190
|
+
keyColor: color,
|
|
27191
|
+
label: chip.label,
|
|
27192
|
+
labelColor: color
|
|
25980
27193
|
};
|
|
25981
27194
|
}, [
|
|
25982
27195
|
activeCacheBreak,
|
|
@@ -26136,7 +27349,7 @@ function AppShell() {
|
|
|
26136
27349
|
initialConfigureKey: authConfigureKey
|
|
26137
27350
|
}),
|
|
26138
27351
|
screen === "sessions" && /* @__PURE__ */ jsx$1(SessionsScreen, {
|
|
26139
|
-
sessions,
|
|
27352
|
+
sessions: sessionsScope === "active" ? sessions.filter((meta) => (sessionPresence.get(meta.id)?.windows ?? 0) > 0) : sessions,
|
|
26140
27353
|
currentId: currentSession?.id ?? null,
|
|
26141
27354
|
focusedSessionId,
|
|
26142
27355
|
onPick: onSwitchSession,
|
|
@@ -26147,8 +27360,11 @@ function AppShell() {
|
|
|
26147
27360
|
contentSearchProgress: sessionContentSearchProgress,
|
|
26148
27361
|
onSearchQueryChange: onSessionSearchQueryChange,
|
|
26149
27362
|
showAllProjects: sessionsShowAll,
|
|
27363
|
+
scopeLabel: sessionsScope === "active" ? "active windows" : sessionsScope === "all" ? "all projects" : void 0,
|
|
27364
|
+
presence: sessionPresence,
|
|
26150
27365
|
onToggleScope: toggleSessionsScope,
|
|
26151
|
-
currentProjectRoot: projectDir
|
|
27366
|
+
currentProjectRoot: projectDir,
|
|
27367
|
+
mainSessionId: settings.orchestration ? mainSessionId : null
|
|
26152
27368
|
}),
|
|
26153
27369
|
screen === "chat" && /* @__PURE__ */ jsx$1(ChatScreen, {
|
|
26154
27370
|
cwd,
|
|
@@ -26161,6 +27377,13 @@ function AppShell() {
|
|
|
26161
27377
|
onEnterQueueFromEmptyPrompt: enterQueueSelectionFromEmptyPrompt,
|
|
26162
27378
|
settings,
|
|
26163
27379
|
onSubmit: onSubmitPrompt,
|
|
27380
|
+
onDraftChange: onPromptDraftChange,
|
|
27381
|
+
remoteDraft,
|
|
27382
|
+
onDraftAttachmentsChange: onPromptDraftAttachmentsChange,
|
|
27383
|
+
remoteDraftAttachments,
|
|
27384
|
+
remoteSearchQuery,
|
|
27385
|
+
remoteSuggest,
|
|
27386
|
+
resolveGhostRemote,
|
|
26164
27387
|
session: currentSession,
|
|
26165
27388
|
pending: pendingApproval,
|
|
26166
27389
|
onApproval: resolveHead,
|
|
@@ -26178,6 +27401,7 @@ function AppShell() {
|
|
|
26178
27401
|
selectedTurnId,
|
|
26179
27402
|
onSelectTurn: onTranscriptSelectTurn,
|
|
26180
27403
|
onOpenTurn: onTranscriptOpenTurn,
|
|
27404
|
+
onOpenSessionEvent: settings.orchestration ? onSwitchSession : void 0,
|
|
26181
27405
|
transcriptSearchMode,
|
|
26182
27406
|
transcriptSearchQuery,
|
|
26183
27407
|
transcriptSearchEvents,
|
|
@@ -26196,6 +27420,7 @@ function AppShell() {
|
|
|
26196
27420
|
}),
|
|
26197
27421
|
/* @__PURE__ */ jsx$1(Footer, {
|
|
26198
27422
|
hints,
|
|
27423
|
+
daemonHealth: settings.useDaemon && !isDaemonDisabledByEnv() ? daemonHealth : null,
|
|
26199
27424
|
context: contextUsage,
|
|
26200
27425
|
cost: footerCost,
|
|
26201
27426
|
status: transcriptSearchMode && transcriptSearchSearching ? "searching" : pendingInteraction?.kind === "question" ? "asking" : compacting ? "compacting" : retryInfo && busy ? "retrying" : busy ? "busy" : null,
|
|
@@ -27638,6 +28863,8 @@ function SkillsSettingsModal({ catalog, onRefresh }) {
|
|
|
27638
28863
|
* undefined behavior. We bail loudly instead.
|
|
27639
28864
|
*/
|
|
27640
28865
|
let runTuiInvoked = false;
|
|
28866
|
+
/** `ZIDANE_DEBUG`-gated sink for the rejection net above `runTui`. */
|
|
28867
|
+
const tuiDebug = createDebugLog("tui");
|
|
27641
28868
|
/**
|
|
27642
28869
|
* Boot a full chat TUI with sensible defaults. **Does not return** under
|
|
27643
28870
|
* normal use — it terminates the host process via `process.exit(0)` once
|
|
@@ -27726,6 +28953,9 @@ async function runTui(options = {}) {
|
|
|
27726
28953
|
if (runTuiInvoked) throw new Error("runTui() can only be invoked once per process. Compose `<App config={resolveConfig(...)} />` against your own renderer if you need to run multiple TUIs in the same lifetime.");
|
|
27727
28954
|
runTuiInvoked = true;
|
|
27728
28955
|
bootTick("runTui:enter");
|
|
28956
|
+
process.on("unhandledRejection", (reason) => {
|
|
28957
|
+
tuiDebug("unhandled rejection", reason);
|
|
28958
|
+
});
|
|
27729
28959
|
registerTreeSitterParsers();
|
|
27730
28960
|
bootTick("runTui:after-registerTreeSitterParsers");
|
|
27731
28961
|
initTreeSitterWorker().catch((err) => {
|
|
@@ -27767,6 +28997,7 @@ async function runTui(options = {}) {
|
|
|
27767
28997
|
const hud = initTuiHud();
|
|
27768
28998
|
const promptBridge = initTuiPromptBridge();
|
|
27769
28999
|
const hostRuntime = initTuiHostRuntime();
|
|
29000
|
+
const bus = initTuiBus();
|
|
27770
29001
|
const extensionRegistry = await discoverAndBuildExtensionRegistry({
|
|
27771
29002
|
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
27772
29003
|
prefix,
|
|
@@ -27778,6 +29009,7 @@ async function runTui(options = {}) {
|
|
|
27778
29009
|
hud,
|
|
27779
29010
|
promptBridge,
|
|
27780
29011
|
hostRuntime,
|
|
29012
|
+
bus,
|
|
27781
29013
|
readSettings: readLiveSettings
|
|
27782
29014
|
});
|
|
27783
29015
|
bootTick("runTui:after-buildExtensionRegistry");
|