zidane 6.5.1 → 6.5.2
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-o-LQAjFw.js → acp-565X8nVP.js} +4 -4
- package/dist/{acp-o-LQAjFw.js.map → acp-565X8nVP.js.map} +1 -1
- package/dist/acp-cli.js +2 -2
- package/dist/acp.d.ts +1 -1
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +1 -1
- package/dist/{agent-BVNkC1h4.js → agent-CvO7UOxv.js} +755 -586
- package/dist/agent-CvO7UOxv.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/chat/pure.d.ts +4 -4
- package/dist/chat/pure.js +3 -3
- package/dist/chat.d.ts +114 -5
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +5 -5
- package/dist/{context-breakdown-BLF10tOO.js → context-segments-1mcqSCM3.js} +110 -2
- package/dist/context-segments-1mcqSCM3.js.map +1 -0
- package/dist/{context-transport-D-daWOmp.js → context-transport-ClvadrlH.js} +3 -3
- package/dist/{context-transport-D-daWOmp.js.map → context-transport-ClvadrlH.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +1 -1
- package/dist/contexts/e2b.d.ts +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/extensions.d.ts +1 -1
- package/dist/extensions.js +1 -1
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -1
- package/dist/{index-CWBSrqI6.d.ts → index-DfERTrql.d.ts} +178 -6
- package/dist/index-DfERTrql.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/{login-CR3fDHXh.js → login-Cl6ML2XN.js} +2 -2
- package/dist/{login-CR3fDHXh.js.map → login-Cl6ML2XN.js.map} +1 -1
- package/dist/{mcp-C6IkXNOS.js → mcp-DdntU47g.js} +3 -3
- package/dist/{mcp-C6IkXNOS.js.map → mcp-DdntU47g.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/output/stream-json.d.ts +1 -1
- package/dist/output/terminal.d.ts +1 -1
- package/dist/plugins.d.ts +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/arcee.d.ts +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/restate.d.ts +1 -1
- package/dist/restate.js +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/{stdio-DzUUkkhS.js → stdio-BQeCMmuK.js} +2 -2
- package/dist/{stdio-DzUUkkhS.js.map → stdio-BQeCMmuK.js.map} +1 -1
- package/dist/{stdio-loader-BILyUGmi.js → stdio-loader-BN1l1ZAG.js} +2 -2
- package/dist/{stdio-loader-BILyUGmi.js.map → stdio-loader-BN1l1ZAG.js.map} +1 -1
- package/dist/{tool-formatters-gFqQbzPY.d.ts → tool-formatters-B2gBbKaS.d.ts} +2 -2
- package/dist/{tool-formatters-gFqQbzPY.d.ts.map → tool-formatters-B2gBbKaS.d.ts.map} +1 -1
- 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-qCs3wZpu.d.ts → transcript-anchors-CeIxhKP7.d.ts} +2 -2
- package/dist/{transcript-anchors-qCs3wZpu.d.ts.map → transcript-anchors-CeIxhKP7.d.ts.map} +1 -1
- package/dist/{transcript-anchors-DEIr0tJC.js → transcript-anchors-CmFKUw8M.js} +138 -18
- package/dist/transcript-anchors-CmFKUw8M.js.map +1 -0
- package/dist/tui.d.ts +74 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +816 -80
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-M9-jUyDd.d.ts → turn-operations-Dyv3Z-Ff.d.ts} +3 -3
- package/dist/{turn-operations-M9-jUyDd.d.ts.map → turn-operations-Dyv3Z-Ff.d.ts.map} +1 -1
- package/dist/{turn-operations-C3IijXMW.js → turn-operations-iGu5J08j.js} +2 -2
- package/dist/{turn-operations-C3IijXMW.js.map → turn-operations-iGu5J08j.js.map} +1 -1
- package/dist/types.d.ts +1 -1
- package/docs/CHAT.md +42 -1
- package/docs/INTERACTIONS.md +39 -0
- package/docs/TUI.md +28 -2
- package/package.json +1 -1
- package/dist/agent-BVNkC1h4.js.map +0 -1
- package/dist/context-breakdown-BLF10tOO.js.map +0 -1
- package/dist/index-CWBSrqI6.d.ts.map +0 -1
- package/dist/transcript-anchors-DEIr0tJC.js.map +0 -1
package/dist/tui.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
import { $t as
|
|
1
|
+
import { $ as McpAuthProvider, $t as filterChainFallbacks, An as discoverAgentsMd, Ar as toolCallPreview, At as useSettings, B as mcpToolsCachePath, C as addToSafelist, Cr as loadState, Ct as DEFAULT_SETTINGS, Dr as stripSpawnTokensLine, Dt as SETTINGS_TOGGLES, Er as serverToolResultSummary, Et as SETTINGS_INPUTS, F as oauthUsesManualCodePaste, G as useMcpToolToggleMap, Gt as useDiscoveryOptional, Ht as createDiscoverySlot, I as runOAuthLogin, J as indexOfServerRow, Jt as resolveConfig, Kt as ConfigProvider, L as supportsOAuth, M as fetchOAuthRedirect, Mr as updateToolEventOutcomes, Nr as usageByModel, Nt as resolveChipColor, Or as sumRunCosts, Ot as SettingsProvider, P as redirectUriFromAuthUrl, Pt as resolveTheme, Qt as chainsEqual, Rn as accentColor, Sr as listSessionMeta, St as useEnabledToggleSet, T as isOnSafelist, Tt as SETTINGS_CHOICES, U as subscribeMcpToolsCache, Ut as DiscoveryProvider, V as refreshMcpToolsCatalog, Wn as TODO_STATUS_GLYPHS, Wt as useDiscovery, X as createFileMcpCredentialStore, Xt as buildRunModel, Y as parentServerName, Yt as resolveStoragePaths, Zn as pruneTodosByRun, Zt as chainPositionOf, _r as compactSummaryEvent, _t as hintsLength, a as useSurfaces, an as countClaudeSessionFiles, at as InteractionsProvider, b as useSafeModeActions, bn as useUpdateCheck, br as eventsFromTurns, c as discoverSystemMd, cn as mcpsWritePath, ct as createInteractionTools, d as userSystemMdPath, dn as buildMcpServers, dr as buildBuildSystem, dt as pendingInteractionsFromTurns, en as providerKeySetChanged, er as useActiveTodos, et as useMcpAuthDispatch, f as useStreamBuffer, fn as dedupMcpServersByName, fr as buildPlanSystem, gn as tryOpenBrowser, gt as clipHintsToWidth, h as discoverProjectSkills, hr as createChatTurnsReadTool, ht as EMPTY_HINTS, i as useSelectStyle, in as useCompletion, it as ASK_USER_TOOL, j as formatPathForCwd, jr as toolResultText, k as suggestSafelistEntries, kn as chatAutoCompactBehavior, kt as clampFps, l as renderSystemMdSeed, ln as removeMcpServer, m as defaultSkillScanPaths, mn as discoverProjectMcps, mr as chatReferenceTokensForPrompt, mt as useInteractionsQueue, n as ThemeProvider, nt as getMcpAuthStatus, on as hasClaudeCodeSetup, ot as PRESENT_PLAN_TOOL, p as buildSkillsConfig, pr as envSection, pt as useInteractionsActions, q as buildVisibleMcpRows, qt as useConfig, r as useColors, rn as toggleChainEntry, s as useTheme, sn as importFromClaudeCode, st as buildResumedToolResultsTurn, t as computeTurnAnchors, tn as restoreChainFallbacks, tt as useMcpAuthState, u as userScopeOverridesLoad, un as upsertMcpServer, ut as makeRequestInteraction, v as writeSessionExport, vn as bootTick, vt as truncateTrailing, w as getSafelist, wr as marginTopFor, wt as SETTINGS_CATEGORIES, x as useSafeModeQueue, xr as lastContextSizeFromTurns, xt as listProjectFiles, y as SafeModeProvider, yn as buildUpdateHint, yr as deriveSessionTitle, yt as buildHints, z as loadMcpToolsCache } from "./transcript-anchors-CmFKUw8M.js";
|
|
2
2
|
import { C as estimateRebilledCostUsd } from "./openai-compat-CnHeo4Lz.js";
|
|
3
|
-
import {
|
|
3
|
+
import { t as buildContextSegments } from "./context-segments-1mcqSCM3.js";
|
|
4
|
+
import { $ as installExtensionFromSpec, $t as groupBindings, A as selectFilesFromSession, C as resolveLegacySessionDir, Cn as resolveExtensionConfigValue, D as resolveTasksDir, Dn as OUTPUT_RESERVE_TOKENS, E as resolveSessionDir, Et as isSpawnTool, G as summaryToTurn, It as looksBinary, M as compactConversation, O as buildPostCompactAttachments, On as effectiveContextWindow, 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, at as createExtensionHud, bt as basic_default, cn as registerExtensionHostModules, dt as filterExtensionRegistry, en as keybindingsPath, f as applyCompactSummaryCutoff, fn as inlineLoadedExtension, ft as filterRegistryPlugins, gn as createDebugLog, hn as scanExtensionDirectories, ht as createExtensionHostRuntimeBridge, it as updateInstalledExtension, lt as composeExtensionPreset, mn as parseExtensionScanPathEnv, mt as partitionExtensionCatalog, on as resolveExtensionKeybindings, ot as discoverAndBuildExtensionRegistry, rt as removeInstalledExtension, t as createAgent, tn as matchesBinding, ut as disposeExtensionRegistry, v as cleanupPersistedSession, w as resolveMcpWarningsDir, wn as sanitizeExtensionConfigActionPatch, x as rehomePersistedStubs, yn as coerceExtensionConfigInput } from "./agent-CvO7UOxv.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-DWVt7MvF.js";
|
|
7
8
|
import { u as errorMessage } from "./errors-BwBxu3zD.js";
|
|
8
|
-
import { R as projectUserPaths, c as SkillInstallError, l as installSkillFromSpec, z as findGitRoot } from "./skills-Cp9vnBwd.js";
|
|
9
|
+
import { E as buildCatalog, R as projectUserPaths, c as SkillInstallError, l as installSkillFromSpec, z as findGitRoot } from "./skills-Cp9vnBwd.js";
|
|
9
10
|
import { n as defaultPluginDataRoot, r as loadAgentPlugin } from "./plugins-2PMTVcp-.js";
|
|
10
11
|
import { a as createRingBufferSink, i as createLoggingHooks, l as logFileTimestamp, n as createFileSink, o as fanoutSink, r as createLogger, s as formatLogLine, t as consoleSink } from "./logger-DBX9uYOw.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 { replaceDynamicSection } from "./prompt.js";
|
|
13
|
+
import { _ as hasAuthorizationHeader, h as McpOAuthProvider, n as connectMcpServers, s as nonOwningMcpView } from "./mcp-DdntU47g.js";
|
|
14
|
+
import { replaceDynamicSection, splitSystemPrompt } from "./prompt.js";
|
|
14
15
|
import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-VpLrNaFQ.js";
|
|
15
16
|
import { n as flattenTurns, r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
|
|
16
17
|
import { o as withTimeout } from "./timeout-BDetbuN6.js";
|
|
17
18
|
import { i as installCacheBreakLogger } from "./cache-telemetry-ehVdWsg2.js";
|
|
18
19
|
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
20
|
import { s as resolveCompletionSubmit } from "./completion-core-BRo86R_L.js";
|
|
20
|
-
import { a as ZIDANE_EXTENSION_SKILL_NAME, d as extensionScaffold, f as readSkillEditMarker, h as withZidaneSkillEditSkill, i as SKILL_EDIT_METADATA_KEY, l as editSessionMetadata, m as withZidaneExtensionSkill, o as ZIDANE_SKILL_EDIT_SKILL_NAME, t as loginMcpServer, u as extensionDirForCwd } from "./login-
|
|
21
|
+
import { a as ZIDANE_EXTENSION_SKILL_NAME, d as extensionScaffold, f as readSkillEditMarker, h as withZidaneSkillEditSkill, i as SKILL_EDIT_METADATA_KEY, l as editSessionMetadata, m as withZidaneExtensionSkill, o as ZIDANE_SKILL_EDIT_SKILL_NAME, t as loginMcpServer, u as extensionDirForCwd } from "./login-Cl6ML2XN.js";
|
|
21
22
|
import { C as restoreModelOptions, S as piIdOf, _ as modelOptionsFor, m as getContextWindow, p as enabledModelOptions, s as setProviderCredential, t as detectAuth, v as modelSupportsReasoning, w as renderDefaultCallbackPage } from "./auth-B8ZuBcKC.js";
|
|
22
23
|
import { createDockerContext } from "./contexts/docker.js";
|
|
23
24
|
import { createTuiStore } from "./session/sqlite.js";
|
|
24
|
-
import { $ as buildContextualDiff, A as createSkillsCompletionProvider, B as EDIT_TOOL_NAMES, C as chunkMarkdownForRender, D as splitMarkdownCodeBlocks, E as normalizeMarkdownCodeFences, G as isVisibleDuringSearch, H as isTurnHighlighted, I as createCommandsCompletionProvider, J as selectableTurnIds, K as promptHistoryFromEvents, L as blendHsl, N as createFilesCompletionProvider, O as stripHtmlCommentsOutsideCode, R as buildLinearRamp, T as codeTokenHasClosingFence, U as isUserBashEvent, V as isEditErrorResult, W as isVisible, X as streamEventSearchText, Y as selectionAfterTurnRemoval, Z as turnSelectionOwnership, _ as turnContextSize, _t as summarizeOutcomes, a as TOOL_DISPLAY, at as filetypeFromPath, b as filterModelCatalog, bt as createChatsCompletionProvider, ct as summarizeEditPayload, d as finalizeStreamingMarkdown, et as buildUnifiedDiff, f as finalizeStreamingMarkdownForOwner, ft as mergeApprovalAndBodyOutcomes, g as revealEarlierTranscriptEvents, gt as stripEditOutcomesAnnotation, h as replaceTranscriptTurnEvents, ht as rewriteMultiEditHeader, i as turnAsText, it as extractEditPayload, j as uniqueSkillNamesFromReferences, mt as resolveApprovalForPayload, n as deleteTurnSafely, o as displayNameFor, ot as previewEditPayload, p as hiddenEarlierTranscriptCount, pt as parseEditOutcomesFromResult, q as retagUserPromptEvents, r as truncateTurnsAt, s as formatToolCall, tt as clipDiffForRender, u as capTranscriptEvents, ut as buildEditOutcomesAnnotation, v as splitPromptSegments, w as closeDanglingCodeFence, y as buildModelCatalog, z as BASH_CALL_ID_PREFIX } from "./turn-operations-
|
|
25
|
+
import { $ as buildContextualDiff, A as createSkillsCompletionProvider, B as EDIT_TOOL_NAMES, C as chunkMarkdownForRender, D as splitMarkdownCodeBlocks, E as normalizeMarkdownCodeFences, G as isVisibleDuringSearch, H as isTurnHighlighted, I as createCommandsCompletionProvider, J as selectableTurnIds, K as promptHistoryFromEvents, L as blendHsl, N as createFilesCompletionProvider, O as stripHtmlCommentsOutsideCode, R as buildLinearRamp, T as codeTokenHasClosingFence, U as isUserBashEvent, V as isEditErrorResult, W as isVisible, X as streamEventSearchText, Y as selectionAfterTurnRemoval, Z as turnSelectionOwnership, _ as turnContextSize, _t as summarizeOutcomes, a as TOOL_DISPLAY, at as filetypeFromPath, b as filterModelCatalog, bt as createChatsCompletionProvider, ct as summarizeEditPayload, d as finalizeStreamingMarkdown, et as buildUnifiedDiff, f as finalizeStreamingMarkdownForOwner, ft as mergeApprovalAndBodyOutcomes, g as revealEarlierTranscriptEvents, gt as stripEditOutcomesAnnotation, h as replaceTranscriptTurnEvents, ht as rewriteMultiEditHeader, i as turnAsText, it as extractEditPayload, j as uniqueSkillNamesFromReferences, mt as resolveApprovalForPayload, n as deleteTurnSafely, o as displayNameFor, ot as previewEditPayload, p as hiddenEarlierTranscriptCount, pt as parseEditOutcomesFromResult, q as retagUserPromptEvents, r as truncateTurnsAt, s as formatToolCall, tt as clipDiffForRender, u as capTranscriptEvents, ut as buildEditOutcomesAnnotation, v as splitPromptSegments, w as closeDanglingCodeFence, y as buildModelCatalog, z as BASH_CALL_ID_PREFIX } from "./turn-operations-iGu5J08j.js";
|
|
25
26
|
import { a as searchHighlightRanges, c as searchSessionHistoryIndexAsync, i as highlightSegments, n as buildSessionSearchIndexAsync, r as filterSessionSearchCatalog, u as searchSessionIndexAsync } from "./history-search-C4bk1Wz_.js";
|
|
26
27
|
import { basename, delimiter, dirname, join, relative, resolve } from "node:path";
|
|
27
28
|
import { Buffer } from "node:buffer";
|
|
@@ -2642,7 +2643,7 @@ const NO_SEARCH_MATCHES = {
|
|
|
2642
2643
|
* needs a stable id on `StreamEvent` itself, which is a data-model change
|
|
2643
2644
|
* rather than a rendering one.
|
|
2644
2645
|
*/
|
|
2645
|
-
function Transcript({ events, settings, selectedTurnId = null, busy = false, searchQuery, searchPending = false, onLoadEarlier, onSelectTurn, onOpenTurn }) {
|
|
2646
|
+
function Transcript({ events, settings, selectedTurnId = null, busy = false, searchQuery, searchPending = false, interactionPending = false, onLoadEarlier, onSelectTurn, onOpenTurn }) {
|
|
2646
2647
|
const COLOR = useColors();
|
|
2647
2648
|
const showThrobber = busy && settings.showThrobber;
|
|
2648
2649
|
const codeSettle = useRef({
|
|
@@ -2766,13 +2767,13 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
|
|
|
2766
2767
|
}, [events]);
|
|
2767
2768
|
const modal = useModalState();
|
|
2768
2769
|
useKeyboard(useCallback((key) => {
|
|
2769
|
-
if (modal?.isOpen) return;
|
|
2770
|
+
if (modal?.isOpen || interactionPending) return;
|
|
2770
2771
|
if (key.name !== "pageup" && key.name !== "pagedown") return;
|
|
2771
2772
|
const scrollbox = scrollboxRef.current;
|
|
2772
2773
|
if (!scrollbox) return;
|
|
2773
2774
|
const direction = key.name === "pageup" ? -1 : 1;
|
|
2774
2775
|
scrollbox.scrollBy(direction, key.shift ? "content" : "viewport");
|
|
2775
|
-
}, [modal]));
|
|
2776
|
+
}, [modal, interactionPending]));
|
|
2776
2777
|
const anchors = useMemo(() => computeTurnAnchors(items), [items]);
|
|
2777
2778
|
const liveByItem = useMemo(() => {
|
|
2778
2779
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -6005,6 +6006,88 @@ function readToolsByServer(dataDir) {
|
|
|
6005
6006
|
return out;
|
|
6006
6007
|
}
|
|
6007
6008
|
//#endregion
|
|
6009
|
+
//#region src/tui/editor-command.ts
|
|
6010
|
+
/**
|
|
6011
|
+
* `$EDITOR` / `$VISUAL` command parsing.
|
|
6012
|
+
*
|
|
6013
|
+
* These variables conventionally hold a command LINE, not a bare executable —
|
|
6014
|
+
* `code --wait`, `cursor --wait`, `subl -w`, `emacsclient -nw` are all normal
|
|
6015
|
+
* values. Spawning the whole string as argv[0] looks for a file literally named
|
|
6016
|
+
* `cursor --wait` and fails with ENOENT, so the flags have to be split off.
|
|
6017
|
+
*
|
|
6018
|
+
* Kept separate from `app.tsx` (and free of `node:child_process`) so the
|
|
6019
|
+
* tokenizer is unit-testable without pulling in the TUI.
|
|
6020
|
+
*/
|
|
6021
|
+
/**
|
|
6022
|
+
* Split an editor command line into `[executable, ...args]`.
|
|
6023
|
+
*
|
|
6024
|
+
* Whitespace-separated, with single and double quotes honored so an editor
|
|
6025
|
+
* living under a path with spaces still works
|
|
6026
|
+
* (`"/Applications/My App/bin/ed" -w`). Quotes group and are stripped;
|
|
6027
|
+
* backslash escaping is deliberately NOT implemented, because on Windows
|
|
6028
|
+
* backslash is the path separator and treating `C:\tools\ed` as escapes would
|
|
6029
|
+
* corrupt far more command lines than it would rescue.
|
|
6030
|
+
*
|
|
6031
|
+
* Returns `[]` for an empty or whitespace-only spec, so callers can fall back
|
|
6032
|
+
* to the platform opener rather than spawning `''`.
|
|
6033
|
+
*/
|
|
6034
|
+
function parseEditorCommand(spec) {
|
|
6035
|
+
const tokens = [];
|
|
6036
|
+
let current = "";
|
|
6037
|
+
let quote = null;
|
|
6038
|
+
let quoted = false;
|
|
6039
|
+
for (const ch of spec) {
|
|
6040
|
+
if (quote) {
|
|
6041
|
+
if (ch === quote) quote = null;
|
|
6042
|
+
else current += ch;
|
|
6043
|
+
continue;
|
|
6044
|
+
}
|
|
6045
|
+
if (ch === "\"" || ch === "'") {
|
|
6046
|
+
quote = ch;
|
|
6047
|
+
quoted = true;
|
|
6048
|
+
continue;
|
|
6049
|
+
}
|
|
6050
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
6051
|
+
if (current !== "" || quoted) {
|
|
6052
|
+
tokens.push(current);
|
|
6053
|
+
current = "";
|
|
6054
|
+
quoted = false;
|
|
6055
|
+
}
|
|
6056
|
+
continue;
|
|
6057
|
+
}
|
|
6058
|
+
current += ch;
|
|
6059
|
+
}
|
|
6060
|
+
if (current !== "" || quoted) tokens.push(current);
|
|
6061
|
+
return tokens;
|
|
6062
|
+
}
|
|
6063
|
+
/**
|
|
6064
|
+
* Re-quote a single argv token for a shell that will re-parse the joined command
|
|
6065
|
+
* line — i.e. `spawn(..., { shell: true })`, which Node uses on Windows so that
|
|
6066
|
+
* `.cmd` / `.bat` wrappers (how `code`, `cursor` and friends ship there) resolve.
|
|
6067
|
+
*
|
|
6068
|
+
* Node does NOT re-quote when `shell` is set: it joins `[cmd, ...args]` with
|
|
6069
|
+
* spaces and hands the string over. Since {@link parseEditorCommand} has already
|
|
6070
|
+
* STRIPPED the user's quotes, handing tokens back unquoted would split
|
|
6071
|
+
* `C:\Program Files\ed.exe` at the space and try to run `C:\Program`. This puts
|
|
6072
|
+
* the quotes back.
|
|
6073
|
+
*
|
|
6074
|
+
* Only tokens that need it are quoted, so a plain `code` stays `code`. A literal
|
|
6075
|
+
* double quote is doubled, which is how `cmd.exe` escapes one inside a quoted
|
|
6076
|
+
* run.
|
|
6077
|
+
*
|
|
6078
|
+
* The test covers `cmd.exe` METACHARACTERS, not just whitespace: `&`, `|`, `^`,
|
|
6079
|
+
* `<`, `>` and friends carry none of the obvious "needs quoting" signals, so a
|
|
6080
|
+
* repo cloned into `C:\work\a&b` would otherwise hand `C:\work\a&b\AGENTS.md`
|
|
6081
|
+
* over bare, and cmd would split it at the `&` and run the tail as a second
|
|
6082
|
+
* command. `%` and `!` are included so a path containing them can't be mistaken
|
|
6083
|
+
* for a variable reference at parse time.
|
|
6084
|
+
*/
|
|
6085
|
+
function quoteForShell(token) {
|
|
6086
|
+
if (token === "") return "\"\"";
|
|
6087
|
+
if (!/[\s"&|^<>%()!]/.test(token)) return token;
|
|
6088
|
+
return `"${token.replace(/"/g, "\"\"")}"`;
|
|
6089
|
+
}
|
|
6090
|
+
//#endregion
|
|
6008
6091
|
//#region src/tui/extension-modal.ts
|
|
6009
6092
|
/**
|
|
6010
6093
|
* Resolve a modal contribution to a renderable node. Eager nodes pass through;
|
|
@@ -8746,7 +8829,7 @@ function InteractionBlock({ request, onResolve }) {
|
|
|
8746
8829
|
onResolve
|
|
8747
8830
|
});
|
|
8748
8831
|
}
|
|
8749
|
-
function InteractionShell({ title, maxHeight, children }) {
|
|
8832
|
+
function InteractionShell({ title, maxHeight, minHeight, children }) {
|
|
8750
8833
|
return /* @__PURE__ */ jsx$1("box", {
|
|
8751
8834
|
title,
|
|
8752
8835
|
style: {
|
|
@@ -8755,14 +8838,48 @@ function InteractionShell({ title, maxHeight, children }) {
|
|
|
8755
8838
|
paddingLeft: 1,
|
|
8756
8839
|
paddingRight: 1,
|
|
8757
8840
|
flexDirection: "column",
|
|
8758
|
-
flexShrink:
|
|
8841
|
+
flexShrink: 1,
|
|
8759
8842
|
overflow: "hidden",
|
|
8760
|
-
...maxHeight !== void 0 ? { maxHeight } : {}
|
|
8843
|
+
...maxHeight !== void 0 ? { maxHeight } : {},
|
|
8844
|
+
minHeight: minHeight ?? 0
|
|
8761
8845
|
},
|
|
8762
8846
|
children
|
|
8763
8847
|
});
|
|
8764
8848
|
}
|
|
8765
8849
|
/**
|
|
8850
|
+
* Resolve a key event to a page-scroll direction, or `null` when it isn't one.
|
|
8851
|
+
*
|
|
8852
|
+
* `pageup` / `pagedown` only, and modifier-free. `shift` is rejected because
|
|
8853
|
+
* `shift+page*` means jump-to-top/bottom for the transcript — a panel must not
|
|
8854
|
+
* quietly redefine it as a page step. `ctrl+u` / `ctrl+d` are deliberately NOT
|
|
8855
|
+
* used: OpenTUI's `defaultTextareaKeyBindings` maps `ctrl+u` to
|
|
8856
|
+
* delete-to-line-start, `ctrl+d` to delete and `ctrl+shift+d` to delete-line,
|
|
8857
|
+
* and a textarea question is focused on exactly the same condition as these
|
|
8858
|
+
* handlers — so those chords would delete the user's answer AND scroll in one
|
|
8859
|
+
* keypress. The page keys appear nowhere in that table.
|
|
8860
|
+
*/
|
|
8861
|
+
function pageScrollDirection(key) {
|
|
8862
|
+
if (key.ctrl || key.meta || key.option || key.shift) return null;
|
|
8863
|
+
if (key.name === "pageup") return -1;
|
|
8864
|
+
if (key.name === "pagedown") return 1;
|
|
8865
|
+
return null;
|
|
8866
|
+
}
|
|
8867
|
+
/**
|
|
8868
|
+
* Scroll one page, keeping a 1-row overlap so the line at the fold stays
|
|
8869
|
+
* visible (standard pager UX).
|
|
8870
|
+
*
|
|
8871
|
+
* Measures `viewport.height`, NOT the renderable's own `height`: the latter
|
|
8872
|
+
* includes anything outside the scrolling viewport, which made the step
|
|
8873
|
+
* overshoot what the user could actually see. `scrollBy` clamps at both ends.
|
|
8874
|
+
*/
|
|
8875
|
+
function pageScrollBy(scrollbox, direction) {
|
|
8876
|
+
const page = Math.max(1, scrollbox.viewport.height - 1);
|
|
8877
|
+
scrollbox.scrollBy({
|
|
8878
|
+
x: 0,
|
|
8879
|
+
y: direction * page
|
|
8880
|
+
});
|
|
8881
|
+
}
|
|
8882
|
+
/**
|
|
8766
8883
|
* The plan BODY half of plan review — pairs with {@link InteractionBlock}
|
|
8767
8884
|
* (which renders only the title + decision picker for `plan` requests).
|
|
8768
8885
|
* The chat screen hides the Transcript (same `visible: false` pattern as
|
|
@@ -8779,15 +8896,10 @@ function PlanReviewPanel({ request }) {
|
|
|
8779
8896
|
const { title, plan, steps } = request.payload;
|
|
8780
8897
|
useKeyboard((key) => {
|
|
8781
8898
|
if (!focused) return;
|
|
8782
|
-
|
|
8783
|
-
if (key.name !== "pageup" && key.name !== "pagedown") return;
|
|
8899
|
+
const direction = pageScrollDirection(key);
|
|
8784
8900
|
const sb = scrollboxRef.current;
|
|
8785
|
-
if (!sb) return;
|
|
8786
|
-
|
|
8787
|
-
sb.scrollBy({
|
|
8788
|
-
x: 0,
|
|
8789
|
-
y: key.name === "pageup" ? -page : page
|
|
8790
|
-
});
|
|
8901
|
+
if (direction === null || !sb) return;
|
|
8902
|
+
pageScrollBy(sb, direction);
|
|
8791
8903
|
});
|
|
8792
8904
|
return /* @__PURE__ */ jsxs("box", {
|
|
8793
8905
|
title: " plan ",
|
|
@@ -8978,30 +9090,70 @@ function PlanInteractionBlock({ request, onResolve }) {
|
|
|
8978
9090
|
}, "pick");
|
|
8979
9091
|
}
|
|
8980
9092
|
/**
|
|
8981
|
-
*
|
|
8982
|
-
*
|
|
8983
|
-
*
|
|
8984
|
-
*
|
|
9093
|
+
* Rows the wizard leaves to the transcript above it, and the smallest cap we
|
|
9094
|
+
* will ever impose.
|
|
9095
|
+
*
|
|
9096
|
+
* The cap used to be `'60%'`. A PERCENTAGE `maxHeight` resolves against the
|
|
9097
|
+
* parent's height, and the wizard's parent is a content-sized column whose
|
|
9098
|
+
* height is indefinite — so Yoga had nothing to take 60% OF and the cap
|
|
9099
|
+
* silently did not apply. The shell then grew to whatever the (under-)estimated
|
|
9100
|
+
* content claimed and drew its border there, which is how questions ended up
|
|
9101
|
+
* rendered below a closed frame. Resolving the cap to a ROW COUNT from the
|
|
9102
|
+
* terminal's own dimensions removes that failure mode entirely.
|
|
8985
9103
|
*/
|
|
8986
|
-
const
|
|
9104
|
+
const WIZARD_RESERVED_ROWS = 8;
|
|
9105
|
+
const WIZARD_MIN_ROWS = 8;
|
|
9106
|
+
/**
|
|
9107
|
+
* Cap the question wizard's height in ROWS, derived from the terminal.
|
|
9108
|
+
*
|
|
9109
|
+
* Deliberately generous (~75%, closer to the plan panel) because a batch of
|
|
9110
|
+
* questions IS the interaction while it's up — the transcript above can wait.
|
|
9111
|
+
* Two guards keep it usable at the extremes: never below
|
|
9112
|
+
* {@link WIZARD_MIN_ROWS} (a 16-row terminal still gets a usable form), and
|
|
9113
|
+
* never within {@link WIZARD_RESERVED_ROWS} of the top (so the user keeps some
|
|
9114
|
+
* transcript context on a tall terminal).
|
|
9115
|
+
*/
|
|
9116
|
+
function wizardMaxRows(terminalRows) {
|
|
9117
|
+
const generous = Math.floor(terminalRows * .75);
|
|
9118
|
+
const leaveRoom = terminalRows - WIZARD_RESERVED_ROWS;
|
|
9119
|
+
return Math.max(WIZARD_MIN_ROWS, Math.min(generous, leaveRoom));
|
|
9120
|
+
}
|
|
9121
|
+
/**
|
|
9122
|
+
* Rows a string occupies once soft-wrapped into `width` columns.
|
|
9123
|
+
*
|
|
9124
|
+
* The sizing estimates used to count `\n` only, with a comment waving off
|
|
9125
|
+
* wrapping as drift the scrollbox would absorb. It doesn't: the estimate feeds
|
|
9126
|
+
* the scrollbox's explicit height, so under-counting makes the panel shorter
|
|
9127
|
+
* than its content and the frame lands mid-text. A wrapped markdown intro or a
|
|
9128
|
+
* wrapped choice description is the common case, not an edge one.
|
|
9129
|
+
*
|
|
9130
|
+
* Word-boundary-agnostic on purpose: `ceil(len / width)` stays within a row of
|
|
9131
|
+
* real word wrapping, and being one row over costs nothing.
|
|
9132
|
+
*
|
|
9133
|
+
* Assumes one column per code point, so CJK and wide emoji (two cells each)
|
|
9134
|
+
* under-count by up to half — the same assumption `truncateToCols` makes in
|
|
9135
|
+
* `settings-modal.tsx`. The consequence is a panel sized short of its content,
|
|
9136
|
+
* not content the user cannot reach: the scrollbox measures its own children for
|
|
9137
|
+
* `scrollHeight`, so the tail stays scrollable either way.
|
|
9138
|
+
*/
|
|
9139
|
+
function wrappedRows(text, width) {
|
|
9140
|
+
if (width <= 0) return text.split("\n").length;
|
|
9141
|
+
let rows = 0;
|
|
9142
|
+
for (const line of text.split("\n")) rows += Math.max(1, Math.ceil(line.length / width));
|
|
9143
|
+
return rows;
|
|
9144
|
+
}
|
|
8987
9145
|
/**
|
|
8988
9146
|
* Rows the intro contributes to the wizard's content-sized height.
|
|
8989
9147
|
*
|
|
8990
|
-
* A line count, plus the one-row bottom margin.
|
|
8991
|
-
*
|
|
8992
|
-
*
|
|
8993
|
-
*
|
|
8994
|
-
* at its ~4-row size.
|
|
9148
|
+
* A wrapped line count, plus the one-row bottom margin. Long intros are capped:
|
|
9149
|
+
* past {@link INTRO_ROWS_CAP} the panel is at its {@link wizardMaxRows} ceiling
|
|
9150
|
+
* anyway and the scrollbox takes over, so counting further buys nothing. Zero
|
|
9151
|
+
* when there is no intro, which keeps a bare confirm question at its ~4-row size.
|
|
8995
9152
|
*/
|
|
8996
|
-
const INTRO_ROWS_CAP =
|
|
8997
|
-
function introRows(intro) {
|
|
9153
|
+
const INTRO_ROWS_CAP = 24;
|
|
9154
|
+
function introRows(intro, width = Number.POSITIVE_INFINITY) {
|
|
8998
9155
|
if (!intro) return 0;
|
|
8999
|
-
|
|
9000
|
-
for (let i = 0; i < intro.length; i++) {
|
|
9001
|
-
if (intro[i] === "\n") lines++;
|
|
9002
|
-
if (lines > INTRO_ROWS_CAP) return 13;
|
|
9003
|
-
}
|
|
9004
|
-
return lines + 1;
|
|
9156
|
+
return Math.min(INTRO_ROWS_CAP, wrappedRows(intro, width)) + 1;
|
|
9005
9157
|
}
|
|
9006
9158
|
/**
|
|
9007
9159
|
* Estimated render height (rows) of one {@link QuestionRow}, mirroring its
|
|
@@ -9011,15 +9163,17 @@ function introRows(intro) {
|
|
|
9011
9163
|
* confirm pair (2), or the 1-line summary / `waiting…` placeholder.
|
|
9012
9164
|
*
|
|
9013
9165
|
* Drives content-sized wizards: a single confirm question claims ~4 rows
|
|
9014
|
-
* instead of stretching to the
|
|
9015
|
-
*
|
|
9016
|
-
* active input out of reach, thanks to scroll-into-view on advance
|
|
9017
|
-
*
|
|
9018
|
-
*
|
|
9166
|
+
* instead of stretching to the {@link wizardMaxRows} ceiling, which stays as
|
|
9167
|
+
* the overflow bound — an under-estimate degrades to scrolling, never clipping
|
|
9168
|
+
* the active input out of reach, thanks to scroll-into-view on advance.
|
|
9169
|
+
*
|
|
9170
|
+
* Prompts, descriptions, and each choice's own description are measured with
|
|
9171
|
+
* {@link wrappedRows} at `width`, so a wrapped line is counted as the rows it
|
|
9172
|
+
* actually occupies.
|
|
9019
9173
|
*/
|
|
9020
|
-
function estimateQuestionRowHeight(question, status) {
|
|
9021
|
-
const
|
|
9022
|
-
let height =
|
|
9174
|
+
function estimateQuestionRowHeight(question, status, width = Number.POSITIVE_INFINITY) {
|
|
9175
|
+
const textWidth = width === Number.POSITIVE_INFINITY ? width : Math.max(1, width - 5);
|
|
9176
|
+
let height = wrappedRows(question.prompt, textWidth) + (question.description ? wrappedRows(question.description, textWidth) : 0) + 1;
|
|
9023
9177
|
if (status !== "active") return height + 1;
|
|
9024
9178
|
switch (question.type) {
|
|
9025
9179
|
case "text":
|
|
@@ -9029,7 +9183,8 @@ function estimateQuestionRowHeight(question, status) {
|
|
|
9029
9183
|
height += 6;
|
|
9030
9184
|
break;
|
|
9031
9185
|
case "select":
|
|
9032
|
-
|
|
9186
|
+
for (const choice of question.choices) height += 1 + (choice.description ? wrappedRows(choice.description, textWidth) : 0);
|
|
9187
|
+
height += question.multi ? 1 : 0;
|
|
9033
9188
|
break;
|
|
9034
9189
|
case "confirm":
|
|
9035
9190
|
height += 2;
|
|
@@ -9077,6 +9232,13 @@ function QuestionInteractionBlock({ request, onResolve }) {
|
|
|
9077
9232
|
if (!canGoBack) return;
|
|
9078
9233
|
setActiveIndex((i) => Math.max(0, i - 1));
|
|
9079
9234
|
});
|
|
9235
|
+
useKeyboard((key) => {
|
|
9236
|
+
if (!focused) return;
|
|
9237
|
+
const direction = pageScrollDirection(key);
|
|
9238
|
+
const scrollbox = scrollboxRef.current;
|
|
9239
|
+
if (direction === null || !scrollbox) return;
|
|
9240
|
+
pageScrollBy(scrollbox, direction);
|
|
9241
|
+
});
|
|
9080
9242
|
useEffect(() => {
|
|
9081
9243
|
const scrollbox = scrollboxRef.current;
|
|
9082
9244
|
if (!scrollbox) return;
|
|
@@ -9087,14 +9249,30 @@ function QuestionInteractionBlock({ request, onResolve }) {
|
|
|
9087
9249
|
});
|
|
9088
9250
|
return () => cancelAnimationFrame(handle);
|
|
9089
9251
|
}, [activeIndex, questions]);
|
|
9090
|
-
const
|
|
9252
|
+
const { width: termCols, height: termRows } = useTerminalDimensions();
|
|
9253
|
+
const maxRows = wizardMaxRows(termRows);
|
|
9254
|
+
const contentCols = Math.max(1, termCols - 4);
|
|
9255
|
+
const measuredRows = useMemo(() => introRows(intro, contentCols) + questions.reduce((sum, q, i) => sum + estimateQuestionRowHeight(q, i < activeIndex ? "done" : i === activeIndex ? "active" : "pending", contentCols), 0), [
|
|
9091
9256
|
intro,
|
|
9092
9257
|
questions,
|
|
9093
|
-
activeIndex
|
|
9258
|
+
activeIndex,
|
|
9259
|
+
contentCols
|
|
9094
9260
|
]);
|
|
9261
|
+
const highWaterRef = useRef({
|
|
9262
|
+
key: "",
|
|
9263
|
+
rows: 0
|
|
9264
|
+
});
|
|
9265
|
+
const highWaterKey = `${request.id}:${contentCols}`;
|
|
9266
|
+
if (highWaterRef.current.key !== highWaterKey) highWaterRef.current = {
|
|
9267
|
+
key: highWaterKey,
|
|
9268
|
+
rows: 0
|
|
9269
|
+
};
|
|
9270
|
+
highWaterRef.current.rows = Math.max(highWaterRef.current.rows, measuredRows);
|
|
9271
|
+
const contentRows = Math.min(highWaterRef.current.rows, maxRows);
|
|
9095
9272
|
return /* @__PURE__ */ jsxs(InteractionShell, {
|
|
9096
9273
|
title: questions.length > 1 ? ` answer ${activeIndex + 1}/${questions.length} · esc to abort run ` : " answer · esc to abort run ",
|
|
9097
|
-
maxHeight:
|
|
9274
|
+
maxHeight: maxRows,
|
|
9275
|
+
minHeight: Math.min(maxRows, contentRows + 2, Math.max(WIZARD_MIN_ROWS, termRows - 2)),
|
|
9098
9276
|
children: [/* @__PURE__ */ jsxs("scrollbox", {
|
|
9099
9277
|
ref: scrollboxRef,
|
|
9100
9278
|
focusable: false,
|
|
@@ -11651,6 +11829,7 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
|
|
|
11651
11829
|
searchQuery: transcriptSearchActive ? transcriptSearchQuery : void 0,
|
|
11652
11830
|
searchPending: transcriptSearchSearching,
|
|
11653
11831
|
busy: !transcriptSearchActive && busy && !pending && !pendingInteraction,
|
|
11832
|
+
interactionPending: pendingInteraction != null,
|
|
11654
11833
|
onLoadEarlier: transcriptSearchActive ? void 0 : onLoadEarlierTranscript
|
|
11655
11834
|
})
|
|
11656
11835
|
}),
|
|
@@ -13810,6 +13989,62 @@ function parentExtensionName(row) {
|
|
|
13810
13989
|
function indexOfExtensionRow(rows, extName) {
|
|
13811
13990
|
return rows.findIndex((r) => r.kind === "extension" && r.ext.name === extName);
|
|
13812
13991
|
}
|
|
13992
|
+
/**
|
|
13993
|
+
* Filter segments by the active query and resolve each one's expanded state.
|
|
13994
|
+
*
|
|
13995
|
+
* Deliberately does NOT auto-expand matches the way the Extensions tab does:
|
|
13996
|
+
* the search corpus includes each segment's full text, so a query typically
|
|
13997
|
+
* matches deep inside a long document — auto-expanding every hit would dump
|
|
13998
|
+
* thousands of lines into the viewport on a keystroke. The user expands the row
|
|
13999
|
+
* they care about.
|
|
14000
|
+
*/
|
|
14001
|
+
function buildVisibleInstructionRows(opts) {
|
|
14002
|
+
const { segments, expanded, queryActive, matchSegment } = opts;
|
|
14003
|
+
const out = [];
|
|
14004
|
+
for (const segment of segments) {
|
|
14005
|
+
if (queryActive && !matchSegment(segment)) continue;
|
|
14006
|
+
out.push({
|
|
14007
|
+
segment,
|
|
14008
|
+
expanded: expanded.has(segment.id)
|
|
14009
|
+
});
|
|
14010
|
+
}
|
|
14011
|
+
return out;
|
|
14012
|
+
}
|
|
14013
|
+
/**
|
|
14014
|
+
* Split a segment's text into renderable preview lines, clamped to
|
|
14015
|
+
* {@link INSTRUCTION_PREVIEW_LINE_CAP}.
|
|
14016
|
+
*
|
|
14017
|
+
* `hidden` is how many lines were dropped off the tail, so the view can render
|
|
14018
|
+
* an honest "+N more lines" marker instead of silently truncating. Tabs are
|
|
14019
|
+
* expanded to two spaces because the terminal text renderable doesn't
|
|
14020
|
+
* interpret `\t`, and `\r` is stripped so CRLF files don't leave artefacts at
|
|
14021
|
+
* every line end.
|
|
14022
|
+
*/
|
|
14023
|
+
function instructionPreviewLines(text) {
|
|
14024
|
+
const all = text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").split("\n");
|
|
14025
|
+
while (all.length > 0 && all[all.length - 1].trim() === "") all.pop();
|
|
14026
|
+
if (all.length <= 400) return {
|
|
14027
|
+
lines: all,
|
|
14028
|
+
hidden: 0
|
|
14029
|
+
};
|
|
14030
|
+
return {
|
|
14031
|
+
lines: all.slice(0, 400),
|
|
14032
|
+
hidden: all.length - 400
|
|
14033
|
+
};
|
|
14034
|
+
}
|
|
14035
|
+
/**
|
|
14036
|
+
* Short badge for a segment's origin — the "where does this come from" column.
|
|
14037
|
+
* `'file'` is intentionally the only one that reads like a location, since it's
|
|
14038
|
+
* the only origin the user can act on.
|
|
14039
|
+
*/
|
|
14040
|
+
function instructionOriginBadge(segment) {
|
|
14041
|
+
switch (segment.origin) {
|
|
14042
|
+
case "file": return "file";
|
|
14043
|
+
case "generated": return "generated";
|
|
14044
|
+
case "mcp": return "mcp server";
|
|
14045
|
+
case "built-in": return "built-in";
|
|
14046
|
+
}
|
|
14047
|
+
}
|
|
13813
14048
|
//#endregion
|
|
13814
14049
|
//#region src/tui/logs-view.ts
|
|
13815
14050
|
/** Level order for the tab's min-level filter (matches the logger's ordering). */
|
|
@@ -14606,13 +14841,14 @@ const TAB_LABELS = {
|
|
|
14606
14841
|
...Object.fromEntries(SETTINGS_CATEGORIES.map((c) => [c.id, c.label])),
|
|
14607
14842
|
keybindings: "Keybindings",
|
|
14608
14843
|
authentication: "Authentication",
|
|
14844
|
+
instructions: "Instructions",
|
|
14609
14845
|
skills: "Skills",
|
|
14610
14846
|
mcps: "MCP servers",
|
|
14611
14847
|
extensions: "Extensions & Plugins",
|
|
14612
14848
|
logs: "Logs"
|
|
14613
14849
|
};
|
|
14614
14850
|
function isCategoryTab(id) {
|
|
14615
|
-
return id !== "skills" && id !== "mcps" && id !== "keybindings" && id !== "authentication" && id !== "extensions" && id !== "logs";
|
|
14851
|
+
return id !== "skills" && id !== "mcps" && id !== "keybindings" && id !== "authentication" && id !== "extensions" && id !== "instructions" && id !== "logs";
|
|
14616
14852
|
}
|
|
14617
14853
|
function anchorIdFor(index) {
|
|
14618
14854
|
return `settings-row-${index}`;
|
|
@@ -14889,6 +15125,16 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
14889
15125
|
const mcpToolToggleMap = useMcpToolToggleMap({ catalogByServer: mcpToolsByServer });
|
|
14890
15126
|
const [expandedMcps, setExpandedMcps] = useState(() => /* @__PURE__ */ new Set());
|
|
14891
15127
|
const [expandedExtensions, setExpandedExtensions] = useState(() => /* @__PURE__ */ new Set());
|
|
15128
|
+
const [expandedInstructions, setExpandedInstructions] = useState(() => /* @__PURE__ */ new Set());
|
|
15129
|
+
const [instructionsNotice, setInstructionsNotice] = useState(null);
|
|
15130
|
+
const [instructionSegments, setInstructionSegments] = useState([]);
|
|
15131
|
+
const [instructionsLoading, setInstructionsLoading] = useState(false);
|
|
15132
|
+
const [instructionsError, setInstructionsError] = useState(void 0);
|
|
15133
|
+
const [instructionsLive, setInstructionsLive] = useState(true);
|
|
15134
|
+
const [instructionsRefreshKey, setInstructionsRefreshKey] = useState(0);
|
|
15135
|
+
const [instructionsShadowedSystem, setInstructionsShadowedSystem] = useState(void 0);
|
|
15136
|
+
const [instructionsCanOverride, setInstructionsCanOverride] = useState(true);
|
|
15137
|
+
const [instructionsOverrideBlockedBy, setInstructionsOverrideBlockedBy] = useState(void 0);
|
|
14892
15138
|
const onRefreshMcpToolsRef = useRef(actions?.onRefreshMcpTools);
|
|
14893
15139
|
onRefreshMcpToolsRef.current = actions?.onRefreshMcpTools;
|
|
14894
15140
|
const [mcpToolsRefreshing, setMcpToolsRefreshing] = useState(false);
|
|
@@ -14905,6 +15151,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
14905
15151
|
const tabs = [...SETTINGS_CATEGORIES.map((c) => c.id)];
|
|
14906
15152
|
if (keybindings) tabs.push("keybindings");
|
|
14907
15153
|
if (authentication) tabs.push("authentication");
|
|
15154
|
+
if (actions?.getContextSegments) tabs.push("instructions");
|
|
14908
15155
|
tabs.push("skills", "mcps");
|
|
14909
15156
|
if (extensionsCatalogProp || extensionsInactiveProp) tabs.push("extensions");
|
|
14910
15157
|
if (logs) tabs.push("logs");
|
|
@@ -14912,6 +15159,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
14912
15159
|
}, [
|
|
14913
15160
|
keybindings,
|
|
14914
15161
|
authentication,
|
|
15162
|
+
actions?.getContextSegments,
|
|
14915
15163
|
extensionsCatalogProp,
|
|
14916
15164
|
extensionsInactiveProp,
|
|
14917
15165
|
logs
|
|
@@ -15090,6 +15338,32 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
15090
15338
|
for (const d of mcpsCatalogLiveRef.current) mcpColdProbedRef.current.add(d.config.name);
|
|
15091
15339
|
fireMcpToolsRefresh();
|
|
15092
15340
|
}, [activeTab, fireMcpToolsRefresh]);
|
|
15341
|
+
useEffect(() => {
|
|
15342
|
+
const fetchSegments = actions?.getContextSegments;
|
|
15343
|
+
if (activeTab !== "instructions" || !fetchSegments) return;
|
|
15344
|
+
let cancelled = false;
|
|
15345
|
+
setInstructionsLoading(true);
|
|
15346
|
+
Promise.resolve(fetchSegments()).then((result) => {
|
|
15347
|
+
if (cancelled) return;
|
|
15348
|
+
setInstructionSegments(result.segments);
|
|
15349
|
+
setInstructionsLive(result.live);
|
|
15350
|
+
setInstructionsCanOverride(result.canOverrideSystem);
|
|
15351
|
+
setInstructionsOverrideBlockedBy(result.overrideBlockedBy);
|
|
15352
|
+
setInstructionsShadowedSystem(result.shadowedSystemPath);
|
|
15353
|
+
setInstructionsError(void 0);
|
|
15354
|
+
}).catch((err) => {
|
|
15355
|
+
if (!cancelled) setInstructionsError(errorMessage(err));
|
|
15356
|
+
}).finally(() => {
|
|
15357
|
+
if (!cancelled) setInstructionsLoading(false);
|
|
15358
|
+
});
|
|
15359
|
+
return () => {
|
|
15360
|
+
cancelled = true;
|
|
15361
|
+
};
|
|
15362
|
+
}, [
|
|
15363
|
+
activeTab,
|
|
15364
|
+
actions?.getContextSegments,
|
|
15365
|
+
instructionsRefreshKey
|
|
15366
|
+
]);
|
|
15093
15367
|
useEffect(() => {
|
|
15094
15368
|
if (activeTab !== "mcps") return;
|
|
15095
15369
|
const cold = mcpsCatalog.filter((d) => mcpsToggle.enabledSet.has(d.config.name) && (mcpToolsByServer[d.config.name]?.length ?? 0) === 0 && !mcpColdProbedRef.current.has(d.config.name));
|
|
@@ -15217,12 +15491,24 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
15217
15491
|
mcpToolsByServer
|
|
15218
15492
|
]);
|
|
15219
15493
|
const filteredProviders = useMemo(() => (authentication?.providers ?? []).filter((p) => matchesTerms(providerCorpus(p), queryTerms)), [authentication?.providers, queryTerms]);
|
|
15494
|
+
const instructionVisibleRows = useMemo(() => buildVisibleInstructionRows({
|
|
15495
|
+
segments: instructionSegments,
|
|
15496
|
+
expanded: expandedInstructions,
|
|
15497
|
+
queryActive,
|
|
15498
|
+
matchSegment: (seg) => matchesTerms(instructionCorpus(seg), queryTerms)
|
|
15499
|
+
}), [
|
|
15500
|
+
instructionSegments,
|
|
15501
|
+
expandedInstructions,
|
|
15502
|
+
queryActive,
|
|
15503
|
+
queryTerms
|
|
15504
|
+
]);
|
|
15220
15505
|
const authRowCount = filteredProviders.length;
|
|
15221
15506
|
const filteredSize = {
|
|
15222
15507
|
...Object.fromEntries(SETTINGS_CATEGORIES.map((c) => [c.id, filteredByCategory[c.id].length])),
|
|
15223
15508
|
skills: filteredSkills.length,
|
|
15224
15509
|
mcps: mcpVisibleRows.length,
|
|
15225
15510
|
extensions: extensionVisibleRows.length,
|
|
15511
|
+
instructions: instructionVisibleRows.length,
|
|
15226
15512
|
keybindings: 0,
|
|
15227
15513
|
logs: 0,
|
|
15228
15514
|
authentication: authRowCount
|
|
@@ -15320,8 +15606,20 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
15320
15606
|
});
|
|
15321
15607
|
}, [extensionVisibleRows]);
|
|
15322
15608
|
const toggleExpandFocusedExtensionRow = useCallback(() => toggleExpandExtensionRowAt(cursor), [toggleExpandExtensionRowAt, cursor]);
|
|
15609
|
+
const toggleExpandInstructionRowAt = useCallback((index) => {
|
|
15610
|
+
const row = instructionVisibleRows[index];
|
|
15611
|
+
if (!row) return;
|
|
15612
|
+
setExpandedInstructions((prev) => {
|
|
15613
|
+
const next = new Set(prev);
|
|
15614
|
+
if (!next.delete(row.segment.id)) next.add(row.segment.id);
|
|
15615
|
+
return next;
|
|
15616
|
+
});
|
|
15617
|
+
}, [instructionVisibleRows]);
|
|
15618
|
+
const toggleExpandFocusedInstructionRow = useCallback(() => toggleExpandInstructionRowAt(cursor), [toggleExpandInstructionRowAt, cursor]);
|
|
15323
15619
|
const mcpRowCount = mcpVisibleRows.length;
|
|
15324
15620
|
const extensionRowCount = extensionVisibleRows.length;
|
|
15621
|
+
const instructionRowCount = instructionVisibleRows.length;
|
|
15622
|
+
const instructionExpandedCount = expandedInstructions.size;
|
|
15325
15623
|
useEffect(() => {
|
|
15326
15624
|
if (activeTab === "keybindings" || activeTab === "logs") return;
|
|
15327
15625
|
const sb = scrollboxRef.current;
|
|
@@ -15345,7 +15643,9 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
15345
15643
|
activeTab,
|
|
15346
15644
|
query,
|
|
15347
15645
|
mcpRowCount,
|
|
15348
|
-
extensionRowCount
|
|
15646
|
+
extensionRowCount,
|
|
15647
|
+
instructionRowCount,
|
|
15648
|
+
instructionExpandedCount
|
|
15349
15649
|
]);
|
|
15350
15650
|
useEffect(() => {
|
|
15351
15651
|
if (mcpRowCount === 0) return;
|
|
@@ -15367,6 +15667,19 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
15367
15667
|
};
|
|
15368
15668
|
});
|
|
15369
15669
|
}, [extensionRowCount]);
|
|
15670
|
+
useEffect(() => {
|
|
15671
|
+
if (instructionRowCount === 0) return;
|
|
15672
|
+
setCursorByTab((prev) => {
|
|
15673
|
+
if (prev.instructions < instructionRowCount) return prev;
|
|
15674
|
+
return {
|
|
15675
|
+
...prev,
|
|
15676
|
+
instructions: instructionRowCount - 1
|
|
15677
|
+
};
|
|
15678
|
+
});
|
|
15679
|
+
}, [instructionRowCount]);
|
|
15680
|
+
useEffect(() => {
|
|
15681
|
+
setInstructionsNotice(null);
|
|
15682
|
+
}, [cursor, activeTab]);
|
|
15370
15683
|
useEffect(() => {
|
|
15371
15684
|
if (activeTab !== "keybindings") return;
|
|
15372
15685
|
const sb = scrollboxRef.current;
|
|
@@ -15466,6 +15779,22 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
15466
15779
|
} else if (row.kind === "tool") mcpToolToggleMap[row.serverName]?.toggle(row.tool.name);
|
|
15467
15780
|
return;
|
|
15468
15781
|
}
|
|
15782
|
+
if (activeTab === "instructions") {
|
|
15783
|
+
const row = instructionVisibleRows[index];
|
|
15784
|
+
if (!row) return;
|
|
15785
|
+
const { segment } = row;
|
|
15786
|
+
if (segment.id === "system" && !segment.path && actions?.onCreateSystemPrompt) {
|
|
15787
|
+
if (!instructionsCanOverride) {
|
|
15788
|
+
setInstructionsNotice(instructionsOverrideBlockedBy === "scope" ? "Your instruction scope excludes user-scope files, so a SYSTEM.md in your home directory would not be loaded. Change it on the Agent tab. Press tab to read the prompt inline." : "This profile supplies its own system prompt — a SYSTEM.md override would not apply. Press tab to read it inline.");
|
|
15789
|
+
return;
|
|
15790
|
+
}
|
|
15791
|
+
Promise.resolve(actions.onCreateSystemPrompt()).catch((err) => setInstructionsNotice(`Could not create SYSTEM.md: ${errorMessage(err)}`));
|
|
15792
|
+
return;
|
|
15793
|
+
}
|
|
15794
|
+
if (segment.path) Promise.resolve(actions?.onOpenInstructionsFile?.(segment.path)).catch((err) => setInstructionsNotice(`Could not open ${segment.label}: ${errorMessage(err)}`));
|
|
15795
|
+
else setInstructionsNotice(`"${segment.label}" is ${instructionOriginBadge(segment)} — no file to open. Press tab to read it inline.`);
|
|
15796
|
+
return;
|
|
15797
|
+
}
|
|
15469
15798
|
if (activeTab === "keybindings") {
|
|
15470
15799
|
actions?.onOpenKeybindings?.();
|
|
15471
15800
|
return;
|
|
@@ -15492,7 +15821,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
15492
15821
|
fireMcpToolsRefresh,
|
|
15493
15822
|
mcpToolToggleMap,
|
|
15494
15823
|
actions,
|
|
15495
|
-
filteredProviders
|
|
15824
|
+
filteredProviders,
|
|
15825
|
+
instructionVisibleRows,
|
|
15826
|
+
instructionsCanOverride,
|
|
15827
|
+
instructionsOverrideBlockedBy
|
|
15496
15828
|
]);
|
|
15497
15829
|
/**
|
|
15498
15830
|
* True while an inline editor or overlay dialog owns the modal's input
|
|
@@ -15549,6 +15881,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
15549
15881
|
activateRowAt(index);
|
|
15550
15882
|
return;
|
|
15551
15883
|
}
|
|
15884
|
+
if (activeTab === "instructions") {
|
|
15885
|
+
toggleExpandInstructionRowAt(index);
|
|
15886
|
+
return;
|
|
15887
|
+
}
|
|
15552
15888
|
if (activeTab === "skills") return;
|
|
15553
15889
|
if (isCategoryTab(activeTab) && filteredByCategory[activeTab][index]?.kind === "toggle") return;
|
|
15554
15890
|
activateRowAt(index);
|
|
@@ -15560,6 +15896,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
15560
15896
|
filteredByCategory,
|
|
15561
15897
|
toggleExpandMcpRowAt,
|
|
15562
15898
|
toggleExpandExtensionRowAt,
|
|
15899
|
+
toggleExpandInstructionRowAt,
|
|
15563
15900
|
activateRowAt
|
|
15564
15901
|
]);
|
|
15565
15902
|
/** Tab-strip clicks — same input-capture guard as row clicks. */
|
|
@@ -15628,6 +15965,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
15628
15965
|
toggleExpandFocusedExtensionRow();
|
|
15629
15966
|
return;
|
|
15630
15967
|
}
|
|
15968
|
+
if (activeTab === "instructions" && key.name === "tab") {
|
|
15969
|
+
toggleExpandFocusedInstructionRow();
|
|
15970
|
+
return;
|
|
15971
|
+
}
|
|
15631
15972
|
if (key.name === "return") {
|
|
15632
15973
|
activateRowAt(cursor);
|
|
15633
15974
|
return;
|
|
@@ -15832,6 +16173,14 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
15832
16173
|
actions.onRefreshSkills();
|
|
15833
16174
|
return;
|
|
15834
16175
|
}
|
|
16176
|
+
if (activeTab === "instructions" && actions?.onRefreshInstructions) {
|
|
16177
|
+
setInstructionsNotice("reloading…");
|
|
16178
|
+
actions.onRefreshInstructions().then((outcome) => {
|
|
16179
|
+
setInstructionsRefreshKey((n) => n + 1);
|
|
16180
|
+
setInstructionsNotice(outcome === "rebuilt" ? "reloaded — agent rebuilt; MCP servers reconnect on the next turn" : outcome === "queued" ? "reloaded — agent rebuild queued until the current run finishes" : "reloaded from disk");
|
|
16181
|
+
}).catch((err) => setInstructionsNotice(`Refresh failed: ${errorMessage(err)}`));
|
|
16182
|
+
return;
|
|
16183
|
+
}
|
|
15835
16184
|
if (activeTab === "extensions" && actions?.onRefreshPlugins) {
|
|
15836
16185
|
actions.onRefreshPlugins();
|
|
15837
16186
|
return;
|
|
@@ -15981,6 +16330,21 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
15981
16330
|
contentCols,
|
|
15982
16331
|
authState
|
|
15983
16332
|
}),
|
|
16333
|
+
activeTab === "instructions" && /* @__PURE__ */ jsx$1(InstructionsList, {
|
|
16334
|
+
onRowClick: clickRowAt,
|
|
16335
|
+
rows: instructionVisibleRows,
|
|
16336
|
+
totalCount: instructionSegments.length,
|
|
16337
|
+
loading: instructionsLoading,
|
|
16338
|
+
error: instructionsError,
|
|
16339
|
+
live: instructionsLive,
|
|
16340
|
+
shadowedSystemPath: instructionsShadowedSystem,
|
|
16341
|
+
canOverrideSystem: instructionsCanOverride,
|
|
16342
|
+
overrideBlockedBy: instructionsOverrideBlockedBy,
|
|
16343
|
+
cursor,
|
|
16344
|
+
highlightBg: SURFACE.selection,
|
|
16345
|
+
query,
|
|
16346
|
+
contentCols
|
|
16347
|
+
}),
|
|
15984
16348
|
activeTab === "keybindings" && keybindings && /* @__PURE__ */ jsx$1(KeybindingsList, {
|
|
15985
16349
|
bindings: keybindings,
|
|
15986
16350
|
extensionRows: extensionKeybindings,
|
|
@@ -16013,6 +16377,13 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
16013
16377
|
style: { flexShrink: 0 },
|
|
16014
16378
|
children: renderMcpDetailPanel(focusedMcp, focusedMcpStatus, COLOR)
|
|
16015
16379
|
}),
|
|
16380
|
+
activeTab === "instructions" && instructionsNotice && /* @__PURE__ */ jsx$1("box", {
|
|
16381
|
+
style: { flexShrink: 0 },
|
|
16382
|
+
children: /* @__PURE__ */ jsx$1("text", {
|
|
16383
|
+
fg: COLOR.mute,
|
|
16384
|
+
children: instructionsNotice
|
|
16385
|
+
})
|
|
16386
|
+
}),
|
|
16016
16387
|
activeTab === "extensions" && extEditNotice && /* @__PURE__ */ jsx$1("box", {
|
|
16017
16388
|
style: { flexShrink: 0 },
|
|
16018
16389
|
children: /* @__PURE__ */ jsx$1("text", {
|
|
@@ -16125,7 +16496,9 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
|
|
|
16125
16496
|
if (row?.kind !== "config" || row.item.type !== "string" || !row.item.secret) return false;
|
|
16126
16497
|
const stored = settings.extensionConfig?.[row.extName]?.[row.item.key];
|
|
16127
16498
|
return stored !== void 0 && stored !== row.item.default;
|
|
16128
|
-
})()
|
|
16499
|
+
})(),
|
|
16500
|
+
focusedInstructionOpenable: activeTab === "instructions" && instructionVisibleRows[cursor]?.segment.path !== void 0,
|
|
16501
|
+
focusedInstructionCreatable: activeTab === "instructions" && actions?.onCreateSystemPrompt !== void 0 && instructionsCanOverride && instructionVisibleRows[cursor]?.segment.id === "system" && instructionVisibleRows[cursor]?.segment.path === void 0
|
|
16129
16502
|
})
|
|
16130
16503
|
}),
|
|
16131
16504
|
extInstallActive && /* @__PURE__ */ jsx$1(ExtensionInstallDialog, {
|
|
@@ -17225,6 +17598,101 @@ function renderEmptyToolsListRow({ serverName, anchorId, focused, bg, COLOR, ref
|
|
|
17225
17598
|
})]
|
|
17226
17599
|
}, `tool-empty:${serverName}`);
|
|
17227
17600
|
}
|
|
17601
|
+
function InstructionsList({ rows, totalCount, loading, error, live, shadowedSystemPath, canOverrideSystem, overrideBlockedBy, cursor, highlightBg, query, contentCols, onRowClick }) {
|
|
17602
|
+
const COLOR = useColors();
|
|
17603
|
+
if (error) return /* @__PURE__ */ jsx$1("text", {
|
|
17604
|
+
fg: COLOR.error,
|
|
17605
|
+
children: `Could not read context: ${error}`
|
|
17606
|
+
});
|
|
17607
|
+
if (loading && totalCount === 0) return /* @__PURE__ */ jsx$1(EmptyRow, { label: "reading session context…" });
|
|
17608
|
+
if (totalCount === 0) return /* @__PURE__ */ jsx$1(EmptyRow, { label: "this profile loads no default context" });
|
|
17609
|
+
if (rows.length === 0) return /* @__PURE__ */ jsx$1(EmptyRow, { label: `no context matches "${query}"` });
|
|
17610
|
+
const INDENT = " ";
|
|
17611
|
+
const detailCols = Math.max(0, contentCols - 1 - 1 - 4);
|
|
17612
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
17613
|
+
style: { flexDirection: "column" },
|
|
17614
|
+
children: [
|
|
17615
|
+
!live && /* @__PURE__ */ jsx$1("text", {
|
|
17616
|
+
fg: COLOR.mute,
|
|
17617
|
+
children: " No active session — showing what your next one will load. MCP and extension context is added at run time."
|
|
17618
|
+
}),
|
|
17619
|
+
shadowedSystemPath !== void 0 && /* @__PURE__ */ jsx$1("text", {
|
|
17620
|
+
fg: COLOR.mute,
|
|
17621
|
+
children: canOverrideSystem ? ` ${displayPath$1(shadowedSystemPath, homedir())} is shadowed by the project SYSTEM.md and is not loaded.` : overrideBlockedBy === "scope" ? ` ${displayPath$1(shadowedSystemPath, homedir())} is not loaded — your instruction scope excludes it.` : ` ${displayPath$1(shadowedSystemPath, homedir())} is not loaded — this profile supplies its own system prompt.`
|
|
17622
|
+
}),
|
|
17623
|
+
rows.map(({ segment, expanded }, i) => {
|
|
17624
|
+
const focused = i === cursor;
|
|
17625
|
+
const bg = focused ? highlightBg : void 0;
|
|
17626
|
+
const openable = segment.path !== void 0;
|
|
17627
|
+
const secondLine = segment.path ? displayPath$1(segment.path, homedir()) : segment.detail ?? "";
|
|
17628
|
+
const preview = expanded ? instructionPreviewLines(segment.text) : null;
|
|
17629
|
+
return /* @__PURE__ */ jsxs("box", {
|
|
17630
|
+
onMouseDown: onRowClick && ((event) => {
|
|
17631
|
+
event.stopPropagation();
|
|
17632
|
+
event.preventDefault();
|
|
17633
|
+
onRowClick(i, "row");
|
|
17634
|
+
}),
|
|
17635
|
+
id: anchorIdFor(i),
|
|
17636
|
+
style: {
|
|
17637
|
+
flexDirection: "column",
|
|
17638
|
+
flexShrink: 0,
|
|
17639
|
+
paddingLeft: 1,
|
|
17640
|
+
paddingRight: 1,
|
|
17641
|
+
backgroundColor: bg
|
|
17642
|
+
},
|
|
17643
|
+
children: [
|
|
17644
|
+
/* @__PURE__ */ jsxs("text", {
|
|
17645
|
+
wrapMode: "none",
|
|
17646
|
+
children: [
|
|
17647
|
+
/* @__PURE__ */ jsx$1("span", {
|
|
17648
|
+
fg: focused ? COLOR.brand : COLOR.mute,
|
|
17649
|
+
children: focused ? "▶ " : " "
|
|
17650
|
+
}),
|
|
17651
|
+
/* @__PURE__ */ jsx$1("span", {
|
|
17652
|
+
fg: COLOR.mute,
|
|
17653
|
+
children: expanded ? "▾ " : "▸ "
|
|
17654
|
+
}),
|
|
17655
|
+
/* @__PURE__ */ jsx$1("span", {
|
|
17656
|
+
fg: focused ? COLOR.brand : COLOR.dim,
|
|
17657
|
+
children: segment.label
|
|
17658
|
+
}),
|
|
17659
|
+
/* @__PURE__ */ jsxs("span", {
|
|
17660
|
+
fg: COLOR.mute,
|
|
17661
|
+
children: [" · ", instructionOriginBadge(segment)]
|
|
17662
|
+
}),
|
|
17663
|
+
/* @__PURE__ */ jsxs("span", {
|
|
17664
|
+
fg: COLOR.mute,
|
|
17665
|
+
children: [" · ~", fmtTokens(segment.tokens)]
|
|
17666
|
+
})
|
|
17667
|
+
]
|
|
17668
|
+
}),
|
|
17669
|
+
secondLine !== "" && /* @__PURE__ */ jsx$1("text", {
|
|
17670
|
+
wrapMode: "none",
|
|
17671
|
+
fg: openable ? COLOR.model : COLOR.mute,
|
|
17672
|
+
children: `${INDENT}${truncateToCols(secondLine, detailCols)}`
|
|
17673
|
+
}),
|
|
17674
|
+
preview && /* @__PURE__ */ jsxs("box", {
|
|
17675
|
+
style: {
|
|
17676
|
+
flexDirection: "column",
|
|
17677
|
+
paddingLeft: 4,
|
|
17678
|
+
paddingTop: 1
|
|
17679
|
+
},
|
|
17680
|
+
children: [preview.lines.map((line, n) => /* @__PURE__ */ jsx$1("text", {
|
|
17681
|
+
wrapMode: "none",
|
|
17682
|
+
fg: COLOR.dim,
|
|
17683
|
+
children: line === "" ? " " : truncateToCols(line, detailCols)
|
|
17684
|
+
}, n)), preview.hidden > 0 && /* @__PURE__ */ jsx$1("text", {
|
|
17685
|
+
wrapMode: "none",
|
|
17686
|
+
fg: COLOR.mute,
|
|
17687
|
+
children: `… +${preview.hidden} more line${preview.hidden === 1 ? "" : "s"}${openable ? " — press ↵ to open the file" : ""}`
|
|
17688
|
+
})]
|
|
17689
|
+
})
|
|
17690
|
+
]
|
|
17691
|
+
}, segment.id);
|
|
17692
|
+
})
|
|
17693
|
+
]
|
|
17694
|
+
});
|
|
17695
|
+
}
|
|
17228
17696
|
function KeybindingsList({ bindings, extensionRows, query }) {
|
|
17229
17697
|
const sections = useMemo(() => groupBindings(bindings, extensionRows), [bindings, extensionRows]);
|
|
17230
17698
|
const filteredSections = useMemo(() => {
|
|
@@ -17713,7 +18181,7 @@ function renderMcpErrors(errors, home, warnColor, dimColor) {
|
|
|
17713
18181
|
}, err.path))
|
|
17714
18182
|
});
|
|
17715
18183
|
}
|
|
17716
|
-
function Hints({ activeTab, authorizing = false, focusedMcp, focusedMcpRow, focusedMcpStatus, canRefreshSkills, canRefreshMcps, canRefreshPlugins, canEditExtension, canCreateExtension, canUpdateExtension, canRemoveExtension, canInstallExtension, canInstallSkill, canEditSkill, canAddMcp, canRemoveSkill, canRemoveMcp, focusedExtensionExpandable, focusedSecretClearable = false }) {
|
|
18184
|
+
function Hints({ activeTab, authorizing = false, focusedMcp, focusedMcpRow, focusedMcpStatus, canRefreshSkills, canRefreshMcps, canRefreshPlugins, canEditExtension, canCreateExtension, canUpdateExtension, canRemoveExtension, canInstallExtension, canInstallSkill, canEditSkill, canAddMcp, canRemoveSkill, canRemoveMcp, focusedExtensionExpandable, focusedSecretClearable = false, focusedInstructionOpenable = false, focusedInstructionCreatable = false }) {
|
|
17717
18185
|
const COLOR = useColors();
|
|
17718
18186
|
const onServerRow = focusedMcpRow?.kind === "server";
|
|
17719
18187
|
const showLogin = activeTab === "mcps" && onServerRow && !!focusedMcp && !!focusedMcpStatus && canLogin$1(focusedMcp, focusedMcpStatus);
|
|
@@ -17737,8 +18205,15 @@ function Hints({ activeTab, authorizing = false, focusedMcp, focusedMcpRow, focu
|
|
|
17737
18205
|
label: "type to filter"
|
|
17738
18206
|
}] : [{
|
|
17739
18207
|
key: "↵",
|
|
17740
|
-
label: isCategoryTab(activeTab) ? "toggle/cycle/select" : activeTab === "keybindings" ? "edit file" : activeTab === "authentication" ? "switch / re-authenticate" : "toggle"
|
|
18208
|
+
label: isCategoryTab(activeTab) ? "toggle/cycle/select" : activeTab === "keybindings" ? "edit file" : activeTab === "authentication" ? "switch / re-authenticate" : activeTab === "instructions" ? focusedInstructionOpenable ? "open file" : focusedInstructionCreatable ? "create SYSTEM.md override" : "no file to open" : "toggle"
|
|
17741
18209
|
}],
|
|
18210
|
+
...activeTab === "instructions" ? [{
|
|
18211
|
+
key: "tab",
|
|
18212
|
+
label: "read inline"
|
|
18213
|
+
}, {
|
|
18214
|
+
key: "ctrl+r",
|
|
18215
|
+
label: "reload"
|
|
18216
|
+
}] : [],
|
|
17742
18217
|
...activeTab === "mcps" && focusedMcpRow ? [{
|
|
17743
18218
|
key: "tab",
|
|
17744
18219
|
label: focusedMcpRow.kind === "server" ? "expand/collapse" : "collapse"
|
|
@@ -17904,6 +18379,21 @@ function keybindingCorpus(row) {
|
|
|
17904
18379
|
if (row.kind === "extension") return `${row.spec} ${row.label} ${row.description} ${row.id}`.toLowerCase();
|
|
17905
18380
|
return `${row.spec} ${row.def.label} ${row.def.description} ${row.def.action}`.toLowerCase();
|
|
17906
18381
|
}
|
|
18382
|
+
/**
|
|
18383
|
+
* Search corpus for an Instructions row — label, origin, detail, path AND the
|
|
18384
|
+
* segment's full text. Including the body is the point: it turns the shared
|
|
18385
|
+
* search box into "which instruction source told the agent this?", which is
|
|
18386
|
+
* the question this tab exists to answer.
|
|
18387
|
+
*/
|
|
18388
|
+
function instructionCorpus(segment) {
|
|
18389
|
+
return [
|
|
18390
|
+
segment.label,
|
|
18391
|
+
instructionOriginBadge(segment),
|
|
18392
|
+
segment.detail ?? "",
|
|
18393
|
+
segment.path ?? "",
|
|
18394
|
+
segment.text
|
|
18395
|
+
].join(" ").toLowerCase();
|
|
18396
|
+
}
|
|
17907
18397
|
function providerCorpus(p) {
|
|
17908
18398
|
return `${p.label} ${p.key} ${p.methods.map((m) => `${m.source} ${m.detail}`).join(" ")}`.toLowerCase();
|
|
17909
18399
|
}
|
|
@@ -18820,29 +19310,45 @@ const debugLog = createDebugLog("tui");
|
|
|
18820
19310
|
* spawned process is detached + unref'd so closing the TUI doesn't
|
|
18821
19311
|
* kill the editor (and vice versa).
|
|
18822
19312
|
*
|
|
18823
|
-
* Resolves once the spawn is
|
|
18824
|
-
* editor to exit. The user is expected to save and either restart
|
|
19313
|
+
* Resolves once the spawn is DISPATCHED (the `'spawn'` event) — we don't wait
|
|
19314
|
+
* for the editor to exit. The user is expected to save and either restart
|
|
18825
19315
|
* the TUI (for changes that need to be picked up at launch, like
|
|
18826
19316
|
* keybindings) or simply continue working.
|
|
19317
|
+
*
|
|
19318
|
+
* Rejects when the spawn fails. `child_process.spawn` reports ENOENT through an
|
|
19319
|
+
* `'error'` EVENT rather than a throw, so a missing binary used to surface as an
|
|
19320
|
+
* unhandled emitter error — crashing into OpenTUI's error console right past
|
|
19321
|
+
* every caller's `try`/`catch`. Funnelling both outcomes into the promise is
|
|
19322
|
+
* what makes those guards effective.
|
|
18827
19323
|
*/
|
|
18828
19324
|
async function launchEditor(path) {
|
|
18829
19325
|
const envEditor = (process.env.VISUAL ?? process.env.EDITOR ?? "").trim();
|
|
19326
|
+
const win32 = process.platform === "win32";
|
|
19327
|
+
const q = win32 ? quoteForShell : (s) => s;
|
|
18830
19328
|
const [cmd, args] = (() => {
|
|
18831
|
-
|
|
19329
|
+
const [bin, ...editorArgs] = parseEditorCommand(envEditor);
|
|
19330
|
+
if (bin) return [q(bin), [...editorArgs.map(q), q(path)]];
|
|
18832
19331
|
if (process.platform === "darwin") return ["open", [path]];
|
|
18833
|
-
if (
|
|
19332
|
+
if (win32) return ["cmd", [
|
|
18834
19333
|
"/c",
|
|
18835
19334
|
"start",
|
|
18836
19335
|
"\"\"",
|
|
18837
|
-
path
|
|
19336
|
+
q(path)
|
|
18838
19337
|
]];
|
|
18839
19338
|
return ["xdg-open", [path]];
|
|
18840
19339
|
})();
|
|
18841
|
-
|
|
18842
|
-
|
|
18843
|
-
|
|
18844
|
-
|
|
18845
|
-
|
|
19340
|
+
await new Promise((resolve, reject) => {
|
|
19341
|
+
const child = spawn(cmd, args, {
|
|
19342
|
+
detached: true,
|
|
19343
|
+
stdio: "ignore",
|
|
19344
|
+
shell: win32
|
|
19345
|
+
});
|
|
19346
|
+
child.once("error", reject);
|
|
19347
|
+
child.once("spawn", () => {
|
|
19348
|
+
child.unref();
|
|
19349
|
+
resolve();
|
|
19350
|
+
});
|
|
19351
|
+
});
|
|
18846
19352
|
}
|
|
18847
19353
|
/**
|
|
18848
19354
|
* Session-metadata key under which the TUI persists the user's "pinned"
|
|
@@ -19056,6 +19562,29 @@ function AppShell() {
|
|
|
19056
19562
|
useEffect(() => {
|
|
19057
19563
|
userInstructionsScopeRef.current = settings.userInstructionsScope;
|
|
19058
19564
|
}, [settings.userInstructionsScope]);
|
|
19565
|
+
const sessionInstructionFilesRef = useRef([]);
|
|
19566
|
+
/**
|
|
19567
|
+
* Path of the `SYSTEM.md` override the CURRENT session's prompt was built
|
|
19568
|
+
* from, or `null` when the built-in doctrine is in force. Same activation-time
|
|
19569
|
+
* semantics as `sessionInstructionFilesRef`: what the live prompt came from,
|
|
19570
|
+
* not what's on disk right now.
|
|
19571
|
+
*/
|
|
19572
|
+
const sessionSystemFileRef = useRef(null);
|
|
19573
|
+
/**
|
|
19574
|
+
* Path of the project-scope override whose warning is already in the visible
|
|
19575
|
+
* transcript, so a rebuild that changes nothing stays quiet. Reset by
|
|
19576
|
+
* `activateSession` because `teardown` clears the transcript the note lived in.
|
|
19577
|
+
*/
|
|
19578
|
+
const announcedSystemOverrideRef = useRef(null);
|
|
19579
|
+
/**
|
|
19580
|
+
* The rendered AGENTS.md block this session's prompt was built with, or `null`
|
|
19581
|
+
* when none loaded. Threaded into `getContextSegments` so the base-prompt
|
|
19582
|
+
* segment can subtract it: the agent receives the block as an opaque substring
|
|
19583
|
+
* of the system string and can't find it on its own, and without this the
|
|
19584
|
+
* instruction text would be reported twice — once inside the system prompt
|
|
19585
|
+
* segment, once as its own file rows.
|
|
19586
|
+
*/
|
|
19587
|
+
const sessionRulesBlockRef = useRef(null);
|
|
19059
19588
|
const subagentModelsRef = useRef(settings.subagentModels);
|
|
19060
19589
|
useEffect(() => {
|
|
19061
19590
|
subagentModelsRef.current = settings.subagentModels;
|
|
@@ -20118,6 +20647,110 @@ function AppShell() {
|
|
|
20118
20647
|
return d ? getContextWindow(d, entry.model) ?? void 0 : void 0;
|
|
20119
20648
|
});
|
|
20120
20649
|
}, [providerRegistry]);
|
|
20650
|
+
/**
|
|
20651
|
+
* The host-composed half of a session's context: the instruction files
|
|
20652
|
+
* discovered for the current cwd + scope, and the system prompt they get
|
|
20653
|
+
* rendered into.
|
|
20654
|
+
*
|
|
20655
|
+
* Extracted from `buildAgent` so session activation and the Settings →
|
|
20656
|
+
* Instructions tab share ONE composition path. The tab has to answer "what
|
|
20657
|
+
* does my context contain" on screens where no agent exists yet (home,
|
|
20658
|
+
* sessions list, model picker — settings opens everywhere but `auth`), and a
|
|
20659
|
+
* second copy of this logic would drift from what sessions actually load.
|
|
20660
|
+
*
|
|
20661
|
+
* Cheap and side-effect-free: a handful of `existsSync` + small reads plus
|
|
20662
|
+
* pure string composition, safe to call on every settings open.
|
|
20663
|
+
*/
|
|
20664
|
+
/**
|
|
20665
|
+
* Warn in the transcript when a PROJECT-scope `SYSTEM.md` is in force.
|
|
20666
|
+
*
|
|
20667
|
+
* Such a file replaces the built-in doctrine — including the destructive-action
|
|
20668
|
+
* and git fragments — so one that merely exists at the root of a checkout can
|
|
20669
|
+
* silently drop "destructive actions need explicit confirmation" and "never
|
|
20670
|
+
* force-push without confirmation". `AGENTS.md` can only APPEND, and renders
|
|
20671
|
+
* under a header marking it as user-authored guidance; deleting doctrine is a
|
|
20672
|
+
* different risk profile from adding to it. The user-scope file stays quiet:
|
|
20673
|
+
* they wrote it themselves, in their own home directory.
|
|
20674
|
+
*
|
|
20675
|
+
* Called from EVERY path that installs a prompt — session activation and
|
|
20676
|
+
* `rebuildActiveAgent` (which serves the Instructions tab's `ctrl+r`, both its
|
|
20677
|
+
* immediate and its deferred-until-idle branch, plus post-login and
|
|
20678
|
+
* extension-swap rebuilds). Announcing only on activation meant that dropping a
|
|
20679
|
+
* `SYSTEM.md` into a checkout and hitting `ctrl+r` swapped the safety guidance
|
|
20680
|
+
* out in silence.
|
|
20681
|
+
*
|
|
20682
|
+
* De-duped on the announced path so an unchanged rebuild doesn't repeat itself.
|
|
20683
|
+
*/
|
|
20684
|
+
const announceProjectSystemOverride = useCallback(() => {
|
|
20685
|
+
const override = sessionSystemFileRef.current;
|
|
20686
|
+
if (override?.source !== "project") {
|
|
20687
|
+
announcedSystemOverrideRef.current = null;
|
|
20688
|
+
return;
|
|
20689
|
+
}
|
|
20690
|
+
if (announcedSystemOverrideRef.current === override.path) return;
|
|
20691
|
+
announcedSystemOverrideRef.current = override.path;
|
|
20692
|
+
stream.appendImmediate({
|
|
20693
|
+
kind: "info",
|
|
20694
|
+
text: `\u26A0 ${override.path} is replacing the built-in system prompt, including the destructive-action and git safety guidance. Settings \u2192 Instructions to read it.`
|
|
20695
|
+
});
|
|
20696
|
+
}, [stream]);
|
|
20697
|
+
const composeHostContext = useCallback((profile, allowInteraction) => {
|
|
20698
|
+
const actualCwd = cwdRef.current;
|
|
20699
|
+
const agentsMd = discoverAgentsMd({
|
|
20700
|
+
cwd: actualCwd,
|
|
20701
|
+
prefix: config.prefix,
|
|
20702
|
+
scope: userInstructionsScopeRef.current,
|
|
20703
|
+
ruler: importRulerConfigsRef.current !== false
|
|
20704
|
+
});
|
|
20705
|
+
const systemMd = discoverSystemMd({
|
|
20706
|
+
cwd: actualCwd,
|
|
20707
|
+
prefix: config.prefix,
|
|
20708
|
+
scope: userInstructionsScopeRef.current
|
|
20709
|
+
});
|
|
20710
|
+
const supportsOverride = profile.id === "build" || profile.id === "plan";
|
|
20711
|
+
const envOpts = {
|
|
20712
|
+
cwd: actualCwd,
|
|
20713
|
+
...projectDir !== actualCwd ? { projectRoot: projectDir } : {},
|
|
20714
|
+
allowInteraction,
|
|
20715
|
+
...agentsMd.block ? { userInstructions: agentsMd.block } : {},
|
|
20716
|
+
...systemMd.file ? { systemOverride: systemMd.file.contents } : {}
|
|
20717
|
+
};
|
|
20718
|
+
const build = profile.id === "build" ? buildBuildSystem : profile.id === "plan" ? buildPlanSystem : null;
|
|
20719
|
+
const system = build ? build(envOpts) : null;
|
|
20720
|
+
return {
|
|
20721
|
+
agentsMd,
|
|
20722
|
+
/** The override actually in force, or `null` when this profile ignores overrides. */
|
|
20723
|
+
systemFile: supportsOverride ? systemMd.file : null,
|
|
20724
|
+
/**
|
|
20725
|
+
* Overrides that exist on disk but aren't loaded. On a profile that can't
|
|
20726
|
+
* accept one, that's EVERY discovered file — not just the precedence
|
|
20727
|
+
* losers — so the tab can explain the silence.
|
|
20728
|
+
*/
|
|
20729
|
+
shadowedSystemFiles: supportsOverride ? systemMd.shadowed : [...systemMd.file ? [systemMd.file] : [], ...systemMd.shadowed],
|
|
20730
|
+
supportsOverride,
|
|
20731
|
+
envOpts,
|
|
20732
|
+
system,
|
|
20733
|
+
/**
|
|
20734
|
+
* The BUILT-IN prompt: no AGENTS.md block, and deliberately no active
|
|
20735
|
+
* `SYSTEM.md` either — the correct seed for a new override.
|
|
20736
|
+
*
|
|
20737
|
+
* Seeding from `system` would bake the instructions into the override and
|
|
20738
|
+
* the next session would append them again (the override replaces the
|
|
20739
|
+
* doctrine; `userInstructions` still composes below it). Clearing
|
|
20740
|
+
* `systemOverride` too means this always answers "what would the built-in
|
|
20741
|
+
* doctrine be", so a future "reset my override" or "diff against default"
|
|
20742
|
+
* caller can't be handed a copy of the override it was trying to replace.
|
|
20743
|
+
*
|
|
20744
|
+
* A thunk so the extra composition only happens on the create path, not
|
|
20745
|
+
* on every session activation.
|
|
20746
|
+
*/
|
|
20747
|
+
doctrineOnlySystem: () => build ? build({
|
|
20748
|
+
...envOpts,
|
|
20749
|
+
userInstructions: void 0,
|
|
20750
|
+
systemOverride: void 0
|
|
20751
|
+
}) : null
|
|
20752
|
+
};
|
|
20753
|
+
}, [config.prefix, projectDir]);
|
|
20121
20754
|
const buildAgent = useCallback((session, key) => {
|
|
20122
20755
|
const descriptor = providerRegistry[key];
|
|
20123
20756
|
if (!descriptor) throw new Error(`No provider registered for key "${key}"`);
|
|
@@ -20178,19 +20811,13 @@ function AppShell() {
|
|
|
20178
20811
|
const allowInteraction = allowInteractionRef.current !== false;
|
|
20179
20812
|
const interactionTools = allowInteraction ? createInteractionTools({ requestInteraction: makeRequestInteraction(interactions) }) : {};
|
|
20180
20813
|
const actualCwd = cwdRef.current;
|
|
20181
|
-
const agentsMd =
|
|
20182
|
-
|
|
20183
|
-
|
|
20184
|
-
|
|
20185
|
-
|
|
20186
|
-
|
|
20187
|
-
|
|
20188
|
-
cwd: actualCwd,
|
|
20189
|
-
...projectDir !== actualCwd ? { projectRoot: projectDir } : {},
|
|
20190
|
-
allowInteraction,
|
|
20191
|
-
...agentsMd.block ? { userInstructions: agentsMd.block } : {}
|
|
20192
|
-
};
|
|
20193
|
-
const builtInSystem = profile.id === "build" ? buildBuildSystem(envOpts) : profile.id === "plan" ? buildPlanSystem(envOpts) : null;
|
|
20814
|
+
const { agentsMd, systemFile, system: builtInSystem } = composeHostContext(profile, allowInteraction);
|
|
20815
|
+
sessionInstructionFilesRef.current = agentsMd.files;
|
|
20816
|
+
sessionRulesBlockRef.current = agentsMd.block;
|
|
20817
|
+
sessionSystemFileRef.current = systemFile ? {
|
|
20818
|
+
path: systemFile.path,
|
|
20819
|
+
source: systemFile.source
|
|
20820
|
+
} : null;
|
|
20194
20821
|
const persistDir = resolvePersistDir({
|
|
20195
20822
|
userDir: cacheDir,
|
|
20196
20823
|
sessionId: session.id
|
|
@@ -21168,6 +21795,8 @@ function AppShell() {
|
|
|
21168
21795
|
sessionRef.current = session;
|
|
21169
21796
|
agentRef.current = buildAgent(session, key);
|
|
21170
21797
|
if (extensionRegistrySwapKeyRef.current === null || extensionRegistrySwapKeyRef.current === extensionRegistry) pendingAgentRebuildRef.current = false;
|
|
21798
|
+
announcedSystemOverrideRef.current = null;
|
|
21799
|
+
announceProjectSystemOverride();
|
|
21171
21800
|
const persistedPins = readPinnedSkills(session.metadata["zidane.activeSkills"]);
|
|
21172
21801
|
setActiveSkillNames(persistedPins);
|
|
21173
21802
|
setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
|
|
@@ -22823,6 +23452,8 @@ function AppShell() {
|
|
|
22823
23452
|
const mcpLoginAbortsRef = useRef(/* @__PURE__ */ new Map());
|
|
22824
23453
|
const buildAgentRef = useRef(buildAgent);
|
|
22825
23454
|
buildAgentRef.current = buildAgent;
|
|
23455
|
+
const announceProjectSystemOverrideRef = useRef(announceProjectSystemOverride);
|
|
23456
|
+
announceProjectSystemOverrideRef.current = announceProjectSystemOverride;
|
|
22826
23457
|
/**
|
|
22827
23458
|
* Rebuild the agent for the active session — used post-login so the
|
|
22828
23459
|
* newly-authenticated MCP server actually connects. The current agent's
|
|
@@ -22843,6 +23474,7 @@ function AppShell() {
|
|
|
22843
23474
|
const staleWarnLog = warnLogRef.current;
|
|
22844
23475
|
const fresh = buildAgentRef.current(session, p.provider.key);
|
|
22845
23476
|
agentRef.current = fresh;
|
|
23477
|
+
announceProjectSystemOverrideRef.current();
|
|
22846
23478
|
if (stale) await stale.destroy().catch((err) => debugLog("rebuildActiveAgent: destroy failed", err));
|
|
22847
23479
|
if (staleWarnLog) await staleWarnLog.close().catch((err) => debugLog("rebuildActiveAgent: warnLog.close failed", err));
|
|
22848
23480
|
}, []);
|
|
@@ -23012,6 +23644,106 @@ function AppShell() {
|
|
|
23012
23644
|
}
|
|
23013
23645
|
})();
|
|
23014
23646
|
}, [modal, config.paths.userDir]);
|
|
23647
|
+
const onReadContextSegments = useCallback(async () => {
|
|
23648
|
+
const { shadowedSystemFiles, supportsOverride } = composeHostContext(pickedAgentRef.current, allowInteractionRef.current !== false);
|
|
23649
|
+
const shadowedSystemPath = shadowedSystemFiles[0]?.path;
|
|
23650
|
+
const scopeLoadsUserOverride = userScopeOverridesLoad(userInstructionsScopeRef.current);
|
|
23651
|
+
const canOverrideSystem = supportsOverride && scopeLoadsUserOverride;
|
|
23652
|
+
const overrideBlockedBy = !supportsOverride ? "profile" : !scopeLoadsUserOverride ? "scope" : void 0;
|
|
23653
|
+
const agent = agentRef.current;
|
|
23654
|
+
if (agent) return {
|
|
23655
|
+
segments: await agent.getContextSegments({
|
|
23656
|
+
rulesFiles: sessionInstructionFilesRef.current,
|
|
23657
|
+
...sessionRulesBlockRef.current ? { rulesBlock: sessionRulesBlockRef.current } : {},
|
|
23658
|
+
...sessionSystemFileRef.current ? { systemPath: sessionSystemFileRef.current.path } : {}
|
|
23659
|
+
}) ?? [],
|
|
23660
|
+
live: true,
|
|
23661
|
+
canOverrideSystem,
|
|
23662
|
+
...overrideBlockedBy ? { overrideBlockedBy } : {},
|
|
23663
|
+
...shadowedSystemPath ? { shadowedSystemPath } : {}
|
|
23664
|
+
};
|
|
23665
|
+
const { agentsMd, systemFile, system } = composeHostContext(pickedAgentRef.current, allowInteractionRef.current !== false);
|
|
23666
|
+
const enabled = enabledSkillsRef.current;
|
|
23667
|
+
const sessionSkills = enabled === void 0 ? [...skillsCatalogRef.current] : skillsCatalogRef.current.filter((s) => enabled.includes(s.name));
|
|
23668
|
+
return {
|
|
23669
|
+
segments: buildContextSegments({
|
|
23670
|
+
...system ? { baseSystem: system } : {},
|
|
23671
|
+
...systemFile ? { systemPath: systemFile.path } : {},
|
|
23672
|
+
...agentsMd.block ? { rulesBlock: agentsMd.block } : {},
|
|
23673
|
+
rulesFiles: agentsMd.files,
|
|
23674
|
+
skillsCatalog: buildCatalog(sessionSkills)
|
|
23675
|
+
}),
|
|
23676
|
+
live: false,
|
|
23677
|
+
canOverrideSystem,
|
|
23678
|
+
...overrideBlockedBy ? { overrideBlockedBy } : {},
|
|
23679
|
+
...shadowedSystemPath ? { shadowedSystemPath } : {}
|
|
23680
|
+
};
|
|
23681
|
+
}, [composeHostContext]);
|
|
23682
|
+
const onOpenInstructionsFile = useCallback(async (path) => {
|
|
23683
|
+
await launchEditor(path);
|
|
23684
|
+
modal.close();
|
|
23685
|
+
}, [modal]);
|
|
23686
|
+
/**
|
|
23687
|
+
* Create `~/.{prefix}/SYSTEM.md` and open it — the `↵` action on the System
|
|
23688
|
+
* prompt row when no override exists yet.
|
|
23689
|
+
*
|
|
23690
|
+
* Seeded with the doctrine currently in effect (see `renderSystemMdSeed`), so
|
|
23691
|
+
* creating the file changes nothing until the user edits it. An empty seed
|
|
23692
|
+
* would strip every line of doctrine the instant the file appeared.
|
|
23693
|
+
*
|
|
23694
|
+
* Idempotent: if the file already exists we open it rather than clobbering the
|
|
23695
|
+
* user's text. `composeHostContext` gives us the exact prompt the next session
|
|
23696
|
+
* would use as the seed source, and we take only the STATIC half so the
|
|
23697
|
+
* per-turn `<env>` block never gets frozen into the file.
|
|
23698
|
+
*/
|
|
23699
|
+
const onCreateSystemPrompt = useCallback(async () => {
|
|
23700
|
+
const path = userSystemMdPath({ prefix: config.prefix });
|
|
23701
|
+
if (!existsSync(path)) {
|
|
23702
|
+
const { doctrineOnlySystem, supportsOverride } = composeHostContext(pickedAgentRef.current, allowInteractionRef.current !== false);
|
|
23703
|
+
if (!supportsOverride) throw new Error(`the "${pickedAgentRef.current.id}" profile supplies its own system prompt, so a SYSTEM.md override would not apply`);
|
|
23704
|
+
if (!userScopeOverridesLoad(userInstructionsScopeRef.current)) throw new Error(`userInstructionsScope is "${userInstructionsScopeRef.current}", so a SYSTEM.md in your home directory would not be loaded`);
|
|
23705
|
+
const doctrine = splitSystemPrompt(doctrineOnlySystem() ?? "").static;
|
|
23706
|
+
if (doctrine.trim().length === 0) throw new Error("could not compose a system prompt to seed the override from");
|
|
23707
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
23708
|
+
writeFileSync(path, renderSystemMdSeed(doctrine), "utf8");
|
|
23709
|
+
}
|
|
23710
|
+
await launchEditor(path);
|
|
23711
|
+
modal.close();
|
|
23712
|
+
}, [
|
|
23713
|
+
modal,
|
|
23714
|
+
config.prefix,
|
|
23715
|
+
composeHostContext
|
|
23716
|
+
]);
|
|
23717
|
+
/**
|
|
23718
|
+
* `ctrl+r` on the Instructions tab: re-read the instruction files from disk
|
|
23719
|
+
* and make the live agent actually use them.
|
|
23720
|
+
*
|
|
23721
|
+
* Re-discovery is implicit — `composeHostContext` hits the filesystem on every
|
|
23722
|
+
* call, so the modal's own re-fetch picks up new files. The work here is
|
|
23723
|
+
* rebuilding the agent so a session that's already running switches to the new
|
|
23724
|
+
* prompt without a TUI restart.
|
|
23725
|
+
*
|
|
23726
|
+
* Deferred while a run is in flight: swapping the system prompt underneath a
|
|
23727
|
+
* streaming turn is not safe, so we raise the same `pendingAgentRebuild` flag
|
|
23728
|
+
* that MCP-settings changes and extension-registry swaps use and let
|
|
23729
|
+
* `drainFollowUps` apply it once the turn ends.
|
|
23730
|
+
*
|
|
23731
|
+
* Returns which of the two happened so the tab can say so — a rebuild that
|
|
23732
|
+
* silently didn't take effect yet is worse than no rebuild.
|
|
23733
|
+
*
|
|
23734
|
+
* Side effect worth surfacing: `rebuildActiveAgent` destroys the stale agent,
|
|
23735
|
+
* which closes its MCP connections. The fresh agent reconnects lazily on its
|
|
23736
|
+
* next run, so the turn after a reload pays that cost. The tab's notice says so.
|
|
23737
|
+
*/
|
|
23738
|
+
const onRefreshInstructions = useCallback(async () => {
|
|
23739
|
+
if (!agentRef.current || !sessionRef.current) return "no-session";
|
|
23740
|
+
if (runningRef.current) {
|
|
23741
|
+
pendingAgentRebuildRef.current = true;
|
|
23742
|
+
return "queued";
|
|
23743
|
+
}
|
|
23744
|
+
await rebuildActiveAgent();
|
|
23745
|
+
return "rebuilt";
|
|
23746
|
+
}, [rebuildActiveAgent]);
|
|
23015
23747
|
const hasMultipleAgents = useMemo(() => Object.keys(liveAgentRegistry).length > 1, [liveAgentRegistry]);
|
|
23016
23748
|
const turnIds = useMemo(() => selectableTurnIds(events, settings), [events, settings]);
|
|
23017
23749
|
useEffect(() => {
|
|
@@ -23688,6 +24420,10 @@ function AppShell() {
|
|
|
23688
24420
|
actions: {
|
|
23689
24421
|
onConfigureProvider,
|
|
23690
24422
|
onOpenKeybindings: onOpenKeybindingsFile,
|
|
24423
|
+
getContextSegments: onReadContextSegments,
|
|
24424
|
+
onOpenInstructionsFile,
|
|
24425
|
+
onCreateSystemPrompt,
|
|
24426
|
+
onRefreshInstructions,
|
|
23691
24427
|
onLoginMcp,
|
|
23692
24428
|
onLogoutMcp,
|
|
23693
24429
|
onCancelLoginMcp,
|