@deepseek-harness-tui/dsh-tui 0.9.3 → 0.10.0-beta.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/README.md +24 -6
- package/cordis.patch.yml +117 -39
- package/cordis.yml +13 -10
- package/lib/types/api.d.ts +7 -0
- package/lib/types/api.d.ts.map +1 -0
- package/lib/types/api.js +1 -0
- package/lib/types/cc/hyperlink.d.ts.map +1 -1
- package/lib/types/cc/hyperlink.js +42 -4
- package/lib/types/cc/markdown.d.ts.map +1 -1
- package/lib/types/cc/markdown.js +15 -5
- package/lib/types/commands.d.ts +6 -0
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +12 -8
- package/lib/types/components/MessageList.d.ts +8 -4
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +94 -32
- package/lib/types/components/PermissionsPicker.d.ts +6 -7
- package/lib/types/components/PermissionsPicker.d.ts.map +1 -1
- package/lib/types/components/PermissionsPicker.js +20 -14
- package/lib/types/components/PromptEditor.d.ts +46 -0
- package/lib/types/components/PromptEditor.d.ts.map +1 -0
- package/lib/types/components/PromptEditor.js +60 -0
- package/lib/types/components/PromptInput.d.ts +12 -0
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +1240 -205
- package/lib/types/components/RewindPicker.js +1 -1
- package/lib/types/components/SplitDiffView.d.ts +11 -1
- package/lib/types/components/SplitDiffView.d.ts.map +1 -1
- package/lib/types/components/SplitDiffView.js +9 -2
- package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
- package/lib/types/components/StreamingMarkdown.js +140 -3
- package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
- package/lib/types/components/SubagentDashboard.js +2 -2
- package/lib/types/components/SubagentDetailScene.js +1 -1
- package/lib/types/components/ThemePicker.d.ts +13 -15
- package/lib/types/components/ThemePicker.d.ts.map +1 -1
- package/lib/types/components/ThemePicker.js +24 -26
- package/lib/types/components/Tooltip.d.ts +67 -0
- package/lib/types/components/Tooltip.d.ts.map +1 -0
- package/lib/types/components/Tooltip.js +220 -0
- package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
- package/lib/types/components/approvals/ApprovalPanel.js +1 -1
- package/lib/types/components/design-system/Divider.d.ts +14 -1
- package/lib/types/components/design-system/Divider.d.ts.map +1 -1
- package/lib/types/components/design-system/Divider.js +86 -8
- package/lib/types/components/design-system/ThemeProvider.d.ts +5 -2
- package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
- package/lib/types/components/design-system/ThemeProvider.js +52 -16
- package/lib/types/components/design-system/ThemedBox.d.ts +23 -2
- package/lib/types/components/design-system/ThemedBox.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts +11 -6
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +10 -6
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +13 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +37 -6
- package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
- package/lib/types/components/messages/UserPromptMessage.js +7 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +1 -1
- package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
- package/lib/types/components/questions/PlanReviewPanel.js +29 -3
- package/lib/types/components/sessions/SessionListRow.d.ts +8 -1
- package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
- package/lib/types/components/sessions/SessionListRow.js +9 -2
- package/lib/types/components/sessions/SessionPreview.d.ts.map +1 -1
- package/lib/types/components/sessions/SessionPreview.js +5 -3
- package/lib/types/components/sessions/WorkspaceListRow.d.ts +17 -0
- package/lib/types/components/sessions/WorkspaceListRow.d.ts.map +1 -0
- package/lib/types/components/sessions/WorkspaceListRow.js +27 -0
- package/lib/types/components/smoothReveal.d.ts +97 -0
- package/lib/types/components/smoothReveal.d.ts.map +1 -0
- package/lib/types/components/smoothReveal.js +279 -0
- package/lib/types/customTheme.d.ts +4 -0
- package/lib/types/customTheme.d.ts.map +1 -1
- package/lib/types/customTheme.js +9 -5
- package/lib/types/dsh-adapter/approvals.d.ts +114 -2
- package/lib/types/dsh-adapter/approvals.d.ts.map +1 -1
- package/lib/types/dsh-adapter/approvals.js +241 -3
- package/lib/types/dsh-adapter/channel.d.ts +59 -2
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +499 -80
- package/lib/types/dsh-adapter/childStderr.d.ts.map +1 -1
- package/lib/types/dsh-adapter/childStderr.js +9 -0
- package/lib/types/dsh-adapter/contract.d.ts +17 -13
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +36 -27
- package/lib/types/dsh-adapter/credentialRefGuard.d.ts +40 -0
- package/lib/types/dsh-adapter/credentialRefGuard.d.ts.map +1 -0
- package/lib/types/dsh-adapter/credentialRefGuard.js +43 -0
- package/lib/types/dsh-adapter/effect-ledger.d.ts +1 -1
- package/lib/types/dsh-adapter/effect-ledger.d.ts.map +1 -1
- package/lib/types/dsh-adapter/effect-ledger.js +1 -0
- package/lib/types/dsh-adapter/extensions.d.ts +4 -1
- package/lib/types/dsh-adapter/extensions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/extensions.js +8 -1
- package/lib/types/dsh-adapter/index.d.ts +11 -1
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +2 -0
- package/lib/types/dsh-adapter/plugin-test-utils.d.ts +62 -0
- package/lib/types/dsh-adapter/plugin-test-utils.d.ts.map +1 -0
- package/lib/types/dsh-adapter/plugin-test-utils.js +61 -0
- package/lib/types/dsh-adapter/plugin.d.ts +20 -0
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +174 -39
- package/lib/types/dsh-adapter/plugins-info.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugins-info.js +12 -0
- package/lib/types/dsh-adapter/preset-resolution.d.ts +49 -0
- package/lib/types/dsh-adapter/preset-resolution.d.ts.map +1 -0
- package/lib/types/dsh-adapter/preset-resolution.js +42 -0
- package/lib/types/dsh-adapter/presets.d.ts +0 -26
- package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/presets.js +4 -14
- package/lib/types/dsh-adapter/promptDebug.d.ts.map +1 -1
- package/lib/types/dsh-adapter/promptDebug.js +4 -1
- package/lib/types/dsh-adapter/providerGuard.d.ts +75 -0
- package/lib/types/dsh-adapter/providerGuard.d.ts.map +1 -0
- package/lib/types/dsh-adapter/providerGuard.js +104 -0
- package/lib/types/dsh-adapter/questions-answerer.d.ts +49 -0
- package/lib/types/dsh-adapter/questions-answerer.d.ts.map +1 -0
- package/lib/types/dsh-adapter/questions-answerer.js +61 -0
- package/lib/types/dsh-adapter/recap.d.ts.map +1 -1
- package/lib/types/dsh-adapter/recap.js +12 -6
- package/lib/types/dsh-adapter/sanitize.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sanitize.js +6 -1
- package/lib/types/dsh-adapter/sessions/digest.d.ts +21 -1
- package/lib/types/dsh-adapter/sessions/digest.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessions/digest.js +229 -3
- package/lib/types/dsh-adapter/sessions/frames.d.ts +3 -0
- package/lib/types/dsh-adapter/sessions/frames.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessions/frames.js +34 -19
- package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessions/list.js +148 -31
- package/lib/types/dsh-adapter/sessions/store.d.ts +8 -0
- package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessions/store.js +24 -4
- package/lib/types/dsh-adapter/sessions/types.d.ts +3 -0
- package/lib/types/dsh-adapter/sessions/types.d.ts.map +1 -1
- package/lib/types/dsh-adapter/settings-sections.d.ts.map +1 -1
- package/lib/types/dsh-adapter/settings-sections.js +13 -1
- package/lib/types/dsh-adapter/settingsEditor.d.ts +6 -0
- package/lib/types/dsh-adapter/settingsEditor.d.ts.map +1 -1
- package/lib/types/dsh-adapter/settingsEditor.js +15 -3
- package/lib/types/dsh-adapter/subagents.d.ts +4 -0
- package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
- package/lib/types/dsh-adapter/subagents.js +8 -0
- package/lib/types/dsh-adapter/themes.d.ts +51 -0
- package/lib/types/dsh-adapter/themes.d.ts.map +1 -0
- package/lib/types/dsh-adapter/themes.js +272 -0
- package/lib/types/dsh-adapter/toast.d.ts +67 -0
- package/lib/types/dsh-adapter/toast.d.ts.map +1 -0
- package/lib/types/dsh-adapter/toast.js +141 -0
- package/lib/types/dsh-adapter/types.d.ts +1 -1
- package/lib/types/extensions.d.ts +4 -0
- package/lib/types/extensions.d.ts.map +1 -1
- package/lib/types/extensions.js +5 -1
- package/lib/types/history.d.ts.map +1 -1
- package/lib/types/history.js +7 -2
- package/lib/types/hooks/useRuntimeThemeSnapshot.d.ts +10 -0
- package/lib/types/hooks/useRuntimeThemeSnapshot.d.ts.map +1 -0
- package/lib/types/hooks/useRuntimeThemeSnapshot.js +32 -0
- package/lib/types/i18n.d.ts +199 -41
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +82 -20
- package/lib/types/ink/components/App.d.ts +20 -0
- package/lib/types/ink/components/App.d.ts.map +1 -1
- package/lib/types/ink/components/App.js +142 -13
- package/lib/types/ink/components/Box.d.ts +39 -2
- package/lib/types/ink/components/Box.d.ts.map +1 -1
- package/lib/types/ink/components/Box.js +61 -37
- package/lib/types/ink/events/context-menu-event.d.ts +19 -0
- package/lib/types/ink/events/context-menu-event.d.ts.map +1 -0
- package/lib/types/ink/events/context-menu-event.js +20 -0
- package/lib/types/ink/events/drag-event.d.ts +44 -0
- package/lib/types/ink/events/drag-event.d.ts.map +1 -0
- package/lib/types/ink/events/drag-event.js +39 -0
- package/lib/types/ink/events/event-handlers.d.ts +8 -0
- package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
- package/lib/types/ink/events/event-handlers.js +8 -0
- package/lib/types/ink/hit-test.d.ts +69 -0
- package/lib/types/ink/hit-test.d.ts.map +1 -1
- package/lib/types/ink/hit-test.js +260 -6
- package/lib/types/ink/ink.d.ts +25 -0
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +74 -10
- package/lib/types/ink/parse-keypress.d.ts +20 -0
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +190 -22
- package/lib/types/ink/renderer.d.ts.map +1 -1
- package/lib/types/ink/renderer.js +15 -1
- package/lib/types/ink/screen.d.ts +7 -0
- package/lib/types/ink/screen.d.ts.map +1 -1
- package/lib/types/ink/screen.js +8 -1
- package/lib/types/ink/termio/osc.d.ts +2 -1
- package/lib/types/ink/termio/osc.d.ts.map +1 -1
- package/lib/types/ink/termio/osc.js +26 -5
- package/lib/types/plugin-spec/registry.d.ts +1 -1
- package/lib/types/plugin-spec/registry.js +1 -1
- package/lib/types/presetPrefs.d.ts +6 -5
- package/lib/types/presetPrefs.d.ts.map +1 -1
- package/lib/types/presetPrefs.js +10 -5
- package/lib/types/screens/Chat.d.ts +4 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +169 -68
- package/lib/types/screens/SessionBrowser.d.ts +9 -4
- package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
- package/lib/types/screens/SessionBrowser.js +483 -75
- package/lib/types/screens/SessionTree.d.ts.map +1 -1
- package/lib/types/screens/SessionTree.js +5 -1
- package/lib/types/screens/Settings.d.ts.map +1 -1
- package/lib/types/screens/Settings.js +5 -1
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +5 -1
- package/lib/types/screens/chatOverlay.d.ts +3 -2
- package/lib/types/screens/chatOverlay.d.ts.map +1 -1
- package/lib/types/screens/chatOverlay.js +2 -0
- package/lib/types/sessionPins.d.ts +33 -0
- package/lib/types/sessionPins.d.ts.map +1 -0
- package/lib/types/sessionPins.js +181 -0
- package/lib/types/sessions/format.d.ts +2 -0
- package/lib/types/sessions/format.d.ts.map +1 -1
- package/lib/types/sessions/format.js +9 -0
- package/lib/types/sessions/view.d.ts +42 -6
- package/lib/types/sessions/view.d.ts.map +1 -1
- package/lib/types/sessions/view.js +133 -35
- package/lib/types/test-utils.d.ts +2 -0
- package/lib/types/test-utils.d.ts.map +1 -0
- package/lib/types/test-utils.js +8 -0
- package/lib/types/theme.d.ts +22 -5
- package/lib/types/theme.d.ts.map +1 -1
- package/lib/types/theme.js +57 -7
- package/lib/types/themeCatalog.d.ts +36 -0
- package/lib/types/themeCatalog.d.ts.map +1 -0
- package/lib/types/themeCatalog.js +157 -0
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +114 -18
- package/lib/types/update.d.ts +159 -3
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +497 -16
- package/lib/types/utils/debug.d.ts.map +1 -1
- package/lib/types/utils/debug.js +2 -1
- package/lib/types/utils/keymap.d.ts +1 -1
- package/lib/types/utils/keymap.d.ts.map +1 -1
- package/lib/types/utils/keymap.js +19 -2
- package/lib/types/utils/urlGuard.d.ts +28 -0
- package/lib/types/utils/urlGuard.d.ts.map +1 -0
- package/lib/types/utils/urlGuard.js +21 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/README.md +3 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.d.ts +7 -11
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.d.ts +16 -1
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.js +23 -2
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.d.ts +5 -5
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/pi-ai.d.ts +23 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/pi-ai.js +27 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.d.ts +17 -3
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.js +36 -4
- package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +11 -12
- package/package.json +114 -74
- package/lib/types/dsh-adapter/packaged-skills.d.ts +0 -19
- package/lib/types/dsh-adapter/packaged-skills.d.ts.map +0 -1
- package/lib/types/dsh-adapter/packaged-skills.js +0 -78
- package/skills/audit/SKILL.md +0 -24
- package/skills/bug/SKILL.md +0 -27
- package/skills/pr-comments/SKILL.md +0 -20
- package/skills/practice/SKILL.md +0 -22
- package/skills/release-notes/SKILL.md +0 -25
- package/skills/review/SKILL.md +0 -25
- package/skills/vuln-check/SKILL.md +0 -25
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { assembleContextFor, installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
3
|
-
import { isUserInvocable, renderSkillContent } from '@deepseek-ai/dsh-skill';
|
|
4
|
-
import { createUserMessage,
|
|
3
|
+
import { isModelInvocable, isUserInvocable, renderSkillContent } from '@deepseek-ai/dsh-skill';
|
|
4
|
+
import { createUserMessage, MessageId, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
5
5
|
import { runSideQuestion, wrapSideQuestion } from './sideQuestion.js';
|
|
6
|
+
import { isReservedCredentialRef } from './credentialRefGuard.js';
|
|
6
7
|
import { collectRecentActivity, parseRecapResponse, RECAP_RECENT_CHARS, wrapRecapPrompt } from './recap.js';
|
|
7
8
|
import { SESSION_COLOR_NAMES } from '../cc/sessionColors.js';
|
|
8
9
|
import { fetchBalance } from '../deepseekBalance.js';
|
|
@@ -11,7 +12,7 @@ import { SessionId } from '@deepseek-ai/dsh-session';
|
|
|
11
12
|
import { renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt';
|
|
12
13
|
import { loadBaselineInstructions } from '@deepseek-ai/dsh-agent-instructions';
|
|
13
14
|
import { extname, isAbsolute, join } from 'node:path';
|
|
14
|
-
import { completeCommands, HIDDEN_COMMAND_NAMES, isLocalCommandName, LOCAL_COMMANDS, parseCommandName } from '../commands.js';
|
|
15
|
+
import { completeCommands, HIDDEN_COMMAND_NAMES, isCommandCompletionToken, isLocalCommandName, LOCAL_COMMANDS, parseCommandName } from '../commands.js';
|
|
15
16
|
import { clearResumeTarget, forgetSession, readResumeTarget, touchSession, writeResumeTarget } from '../sessionHistory.js';
|
|
16
17
|
import { appendSessionTitle, defaultMaxScanned, deleteSessionLog, ensureLegacySessionEventTypes, readSessionEventsFromFile, readSessionEventsFromLog, sessionsRoots } from './compat/index.js';
|
|
17
18
|
import { buildSessionTree, forkTarget, liveTailWindow, rewindTarget, turnUserText, } from './sessionTree.js';
|
|
@@ -22,16 +23,17 @@ import { isPathLikeQuery, rankFileCandidates } from '../utils/fileSuggestions.js
|
|
|
22
23
|
import { readEffortPref, writeEffortPref } from '../effortPrefs.js';
|
|
23
24
|
import { readModelPref, writeModelPref } from '../modelPrefs.js';
|
|
24
25
|
import { explicitModelRoute, recordedModelRoute, resolveModelRoute, validateModelRoute } from '../modelRoute.js';
|
|
25
|
-
import { readPresetPref, writePresetPref } from '../presetPrefs.js';
|
|
26
|
-
import { composePreset, resolvePersistedPreset, resolvePersistedRoute,
|
|
26
|
+
import { migratePresetPref, readPresetPref, writePresetPref } from '../presetPrefs.js';
|
|
27
|
+
import { composePreset, resolvePersistedPreset, resolvePersistedRoute, runningPresetOf, serviceForAgent } from './presets.js';
|
|
28
|
+
import { resolveCompatiblePreset, rosterOf } from './preset-resolution.js';
|
|
27
29
|
import { isPresetName, PRESET_NAMES } from '../components/activityFrames.js';
|
|
28
30
|
import { existsSync, statSync, writeFileSync } from 'node:fs';
|
|
29
31
|
import { logForDebugging } from '../utils/debug.js';
|
|
30
32
|
import { homeDir, LEGACY_DATA_DIR } from '../utils/paths.js';
|
|
31
33
|
import { extractMentions } from '../utils/mentions.js';
|
|
32
34
|
import { getLang, LANGS, t } from '../i18n.js';
|
|
33
|
-
import { AUTO_THEME_NAME
|
|
34
|
-
import {
|
|
35
|
+
import { AUTO_THEME_NAME } from '../theme.js';
|
|
36
|
+
import { listThemeCatalog } from '../themeCatalog.js';
|
|
35
37
|
import { modeDisplayName, resolveSessionModes } from '../sessionModes.js';
|
|
36
38
|
import { normalizeScrollGutter, normalizeStatusBar, normalizeToolBackground } from '../tuiDisplayPrefs.js';
|
|
37
39
|
import { SubagentActivityStore } from './subagents.js';
|
|
@@ -45,6 +47,7 @@ import { getHostCommandTrees } from './command-trees.js';
|
|
|
45
47
|
import { getHostSettingsSections, getLocalSettingsSectionsHost } from './settings-sections.js';
|
|
46
48
|
import { getHostSceneRuntime } from './scenes.js';
|
|
47
49
|
import { getHostRenderers } from './renderers.js';
|
|
50
|
+
import { getHostThemes } from './themes.js';
|
|
48
51
|
import { getHostMessageObserver } from './message-observer.js';
|
|
49
52
|
import { dispatchTuiDecision, dispatchTuiNotification, normalizeCancelDecision } from './extension-events.js';
|
|
50
53
|
import { installDecisionGuard } from './decision-guard.js';
|
|
@@ -131,6 +134,119 @@ function normalizeRewindPromptDecision(result, warn) {
|
|
|
131
134
|
/** Toast-bound plugin text (veto reasons, handled notices, rewind summaries)
|
|
132
135
|
* is render-path data too: same sanitization, toast-width cap. */
|
|
133
136
|
const NOTICE_CELLS = 200;
|
|
137
|
+
const PERMISSION_PRESET_CUSTOM = 'custom';
|
|
138
|
+
const PERMISSION_PRESET_NAME_CELLS = 120;
|
|
139
|
+
const PERMISSION_PRESET_DESCRIPTION_CELLS = 400;
|
|
140
|
+
function isRecord(value) {
|
|
141
|
+
return value !== null && typeof value === 'object';
|
|
142
|
+
}
|
|
143
|
+
function legacyPermissionPresetOptions() {
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
value: 'read-only',
|
|
147
|
+
name: t('permission-preset-readonly'),
|
|
148
|
+
description: t('permission-preset-readonly-desc'),
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
value: 'workspace-write',
|
|
152
|
+
name: t('permission-preset-workspace-write'),
|
|
153
|
+
description: t('permission-preset-workspace-write-desc'),
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
value: 'danger-full-access',
|
|
157
|
+
name: t('permission-preset-full-access'),
|
|
158
|
+
description: t('permission-preset-full-access-desc'),
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
function legacyPermissionPresetSnapshot(sandbox) {
|
|
163
|
+
const options = legacyPermissionPresetOptions();
|
|
164
|
+
const currentOption = sandbox === undefined ? undefined : options.find(option => option.value === sandbox);
|
|
165
|
+
return {
|
|
166
|
+
availability: 'legacy',
|
|
167
|
+
options,
|
|
168
|
+
...(currentOption === undefined
|
|
169
|
+
? {}
|
|
170
|
+
: { current: { ...currentOption, kind: 'preset' } }),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function unavailablePermissionPresetSnapshot() {
|
|
174
|
+
return { availability: 'unavailable', options: [] };
|
|
175
|
+
}
|
|
176
|
+
function normalizePermissionPresetOption(value) {
|
|
177
|
+
if (!isRecord(value) || typeof value.value !== 'string' || typeof value.name !== 'string')
|
|
178
|
+
return undefined;
|
|
179
|
+
const name = cleanRenderText(value.name, PERMISSION_PRESET_NAME_CELLS);
|
|
180
|
+
if (name === '')
|
|
181
|
+
return undefined;
|
|
182
|
+
if (value.description !== undefined && typeof value.description !== 'string')
|
|
183
|
+
return undefined;
|
|
184
|
+
const description = value.description === undefined
|
|
185
|
+
? undefined
|
|
186
|
+
: cleanRenderText(value.description, PERMISSION_PRESET_DESCRIPTION_CELLS);
|
|
187
|
+
if (value.description !== undefined && description === '')
|
|
188
|
+
return undefined;
|
|
189
|
+
return {
|
|
190
|
+
value: value.value,
|
|
191
|
+
name,
|
|
192
|
+
...(description === undefined || description === '' ? {} : { description }),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function permissionPresetSnapshotFromService(service, events) {
|
|
196
|
+
if (!isRecord(service))
|
|
197
|
+
return unavailablePermissionPresetSnapshot();
|
|
198
|
+
const runtime = service;
|
|
199
|
+
try {
|
|
200
|
+
const capturedNames = runtime.names;
|
|
201
|
+
const current = runtime.current;
|
|
202
|
+
const optionOf = runtime.optionOf;
|
|
203
|
+
if (!Array.isArray(capturedNames) || capturedNames.length === 0)
|
|
204
|
+
return unavailablePermissionPresetSnapshot();
|
|
205
|
+
if (typeof current !== 'function' || typeof optionOf !== 'function')
|
|
206
|
+
return unavailablePermissionPresetSnapshot();
|
|
207
|
+
const names = [...capturedNames];
|
|
208
|
+
const seen = new Set();
|
|
209
|
+
for (const name of names) {
|
|
210
|
+
if (typeof name !== 'string' || name.trim() === '' || name === PERMISSION_PRESET_CUSTOM || seen.has(name)) {
|
|
211
|
+
return unavailablePermissionPresetSnapshot();
|
|
212
|
+
}
|
|
213
|
+
seen.add(name);
|
|
214
|
+
}
|
|
215
|
+
const options = [];
|
|
216
|
+
for (const name of names) {
|
|
217
|
+
const option = normalizePermissionPresetOption(optionOf(name));
|
|
218
|
+
if (option === undefined || option.value !== name)
|
|
219
|
+
return unavailablePermissionPresetSnapshot();
|
|
220
|
+
options.push({ ...option });
|
|
221
|
+
}
|
|
222
|
+
const currentValue = current(events);
|
|
223
|
+
if (typeof currentValue !== 'string' || (currentValue !== PERMISSION_PRESET_CUSTOM && !seen.has(currentValue))) {
|
|
224
|
+
return unavailablePermissionPresetSnapshot();
|
|
225
|
+
}
|
|
226
|
+
const currentOption = normalizePermissionPresetOption(optionOf(currentValue));
|
|
227
|
+
if (currentOption === undefined || currentOption.value !== currentValue)
|
|
228
|
+
return unavailablePermissionPresetSnapshot();
|
|
229
|
+
if (currentValue !== PERMISSION_PRESET_CUSTOM) {
|
|
230
|
+
const rosterOption = options.find(option => option.value === currentValue);
|
|
231
|
+
if (rosterOption === undefined
|
|
232
|
+
|| rosterOption.name !== currentOption.name
|
|
233
|
+
|| rosterOption.description !== currentOption.description) {
|
|
234
|
+
return unavailablePermissionPresetSnapshot();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
availability: 'runtime',
|
|
239
|
+
options,
|
|
240
|
+
current: {
|
|
241
|
+
...currentOption,
|
|
242
|
+
kind: currentValue === PERMISSION_PRESET_CUSTOM ? 'custom' : 'preset',
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return unavailablePermissionPresetSnapshot();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
134
250
|
/** `tui/rewind-done` return normalization: the first non-empty STRING is the
|
|
135
251
|
* summary; anything else is not a decision. */
|
|
136
252
|
function normalizeRewindDoneSummary(result, warn) {
|
|
@@ -173,6 +289,22 @@ const SUBAGENT_TOOL_NAMES = new Set([
|
|
|
173
289
|
function isSubagentToolName(name) {
|
|
174
290
|
return SUBAGENT_TOOL_NAMES.has(name.toLowerCase());
|
|
175
291
|
}
|
|
292
|
+
/** Narrow an optional plugin event without importing its module augmentation. */
|
|
293
|
+
function todoPanelItems(data) {
|
|
294
|
+
if (typeof data !== 'object' || data === null)
|
|
295
|
+
return undefined;
|
|
296
|
+
const todos = data.todos;
|
|
297
|
+
if (!Array.isArray(todos))
|
|
298
|
+
return undefined;
|
|
299
|
+
const valid = todos.every(item => {
|
|
300
|
+
if (typeof item !== 'object' || item === null)
|
|
301
|
+
return false;
|
|
302
|
+
const candidate = item;
|
|
303
|
+
return typeof candidate.content === 'string' &&
|
|
304
|
+
(candidate.status === 'pending' || candidate.status === 'in_progress' || candidate.status === 'completed');
|
|
305
|
+
});
|
|
306
|
+
return valid ? todos : undefined;
|
|
307
|
+
}
|
|
176
308
|
const ARGS_PREVIEW_LIMIT = 160;
|
|
177
309
|
const RESULT_PREVIEW_LIMIT = 240;
|
|
178
310
|
/** Local `!`-command output cap (mirrors the result preview limit). */
|
|
@@ -508,6 +640,7 @@ async function waitForTurnEnd(session, fromSeq, timeoutMs) {
|
|
|
508
640
|
export function createChannel(ctx, initialAgent, options) {
|
|
509
641
|
let agent = initialAgent;
|
|
510
642
|
let currentHandle = options.handle;
|
|
643
|
+
const themeHost = getHostThemes(ctx.get('tuiThemes'));
|
|
511
644
|
const subagentControl = {
|
|
512
645
|
interrupt(agentId) {
|
|
513
646
|
const child = subagentStore.get(agentId);
|
|
@@ -662,6 +795,25 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
662
795
|
state.subagents = subagentStore.snapshot();
|
|
663
796
|
syncSubagentRows(state.subagents);
|
|
664
797
|
};
|
|
798
|
+
/** Drop the subagent row map (transcript wipe): the next event for a still
|
|
799
|
+
* live subagent re-creates its card as a fresh row instead of feeding a
|
|
800
|
+
* row object no transcript holds (update-only orphan). */
|
|
801
|
+
const dropSubagentRows = () => {
|
|
802
|
+
subagentStreamDirty = false;
|
|
803
|
+
subagentRowsByAgentId.clear();
|
|
804
|
+
};
|
|
805
|
+
/** Full subagent reset for a session swap: the row map, the queued task
|
|
806
|
+
* descriptions and the store itself are all scoped to the OLD agent's
|
|
807
|
+
* session. Leaked into the adopted one, they would keep dead subagents in
|
|
808
|
+
* the dashboard snapshot until new events overwrite it, grow the row map
|
|
809
|
+
* without bound across swaps, and hand a stale queued description to the
|
|
810
|
+
* new session's first card. */
|
|
811
|
+
const resetSubagentProjection = () => {
|
|
812
|
+
dropSubagentRows();
|
|
813
|
+
pendingTaskDescriptions.length = 0;
|
|
814
|
+
subagentStore.reset();
|
|
815
|
+
state.subagents = [];
|
|
816
|
+
};
|
|
665
817
|
// foldRows incremental cursor (see foldRows): rows only append past the
|
|
666
818
|
// fold line, so each pass touches only newly-eligible rows.
|
|
667
819
|
const foldCursor = { rows: null, index: 0 };
|
|
@@ -906,6 +1058,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
906
1058
|
toolCards.clear();
|
|
907
1059
|
nextRowId = 0;
|
|
908
1060
|
state.rows.length = 0;
|
|
1061
|
+
resetSubagentProjection();
|
|
909
1062
|
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
910
1063
|
// the session being entered (or leaves them empty).
|
|
911
1064
|
state.todos = [];
|
|
@@ -958,6 +1111,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
958
1111
|
touchSession(childId);
|
|
959
1112
|
state.emit();
|
|
960
1113
|
void oldHandle?.dispose().catch(() => { });
|
|
1114
|
+
// The staged-image map is session-scoped (the same contract the
|
|
1115
|
+
// resumeTo/newSession tails enforce): tokens typed against the rewound
|
|
1116
|
+
// conversation must not ride into the fork's next send, and the epoch
|
|
1117
|
+
// bump fences saves still in flight for the old session.
|
|
1118
|
+
clearStagedImages();
|
|
961
1119
|
return sourceSessionId;
|
|
962
1120
|
};
|
|
963
1121
|
/** Monotonic token: only the latest `interruptAndDeliver` re-queues, so a
|
|
@@ -1176,7 +1334,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1176
1334
|
}
|
|
1177
1335
|
// `undefined` = not registered; a handler error surfaces as its
|
|
1178
1336
|
// message so the user sees why the command failed.
|
|
1179
|
-
return execution
|
|
1337
|
+
return execution === undefined ? undefined : execution.result.text ?? '';
|
|
1180
1338
|
}
|
|
1181
1339
|
catch (error) {
|
|
1182
1340
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1389,6 +1547,39 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1389
1547
|
state.emit();
|
|
1390
1548
|
}).catch(() => { });
|
|
1391
1549
|
};
|
|
1550
|
+
// --- Manual-compaction lifecycle ---------------------------------------
|
|
1551
|
+
// The in-flight /compact transaction: its abort hook plus the settled
|
|
1552
|
+
// promise. Every path that replaces `agent` (rewind / rewind-node /
|
|
1553
|
+
// resume / new / model switch) must cancel and await it BEFORE snapshot-
|
|
1554
|
+
// ting the session. Without this, a slow summarizer keeps running against
|
|
1555
|
+
// the OLD session across the switch and can commit its replacement
|
|
1556
|
+
// checkpoint AFTER the fork snapshot — silently swapping the history the
|
|
1557
|
+
// user believed intact ("compaction failed → /model → context lost").
|
|
1558
|
+
let manualCompaction;
|
|
1559
|
+
/** Compactions cancelled by settleManualCompaction: their rejection is expected. */
|
|
1560
|
+
const cancelledCompactions = new WeakSet();
|
|
1561
|
+
/**
|
|
1562
|
+
* Cancel an in-flight manual compaction and wait for it to settle.
|
|
1563
|
+
* Aborting tears the summarizer stream down; dsh-compaction then closes
|
|
1564
|
+
* the transaction with an error end marker and rejects compactNow with
|
|
1565
|
+
* the `cancelled` class — no checkpoint is committed, the surface stays
|
|
1566
|
+
* whole. The settle race is capped so a stuck stream can never wedge the
|
|
1567
|
+
* session switch itself.
|
|
1568
|
+
*/
|
|
1569
|
+
const settleManualCompaction = async () => {
|
|
1570
|
+
const active = manualCompaction;
|
|
1571
|
+
if (active === undefined)
|
|
1572
|
+
return;
|
|
1573
|
+
manualCompaction = undefined;
|
|
1574
|
+
cancelledCompactions.add(active.controller);
|
|
1575
|
+
active.controller.abort(new Error('session switch'));
|
|
1576
|
+
state.notify(t('compact-cancelled-switch'), { color: 'warning', timeoutMs: 4000 });
|
|
1577
|
+
await Promise.race([
|
|
1578
|
+
active.settled,
|
|
1579
|
+
new Promise(resolve => { setTimeout(resolve, 3000); }),
|
|
1580
|
+
]);
|
|
1581
|
+
};
|
|
1582
|
+
let agentBindingGeneration = 0;
|
|
1392
1583
|
const state = {
|
|
1393
1584
|
effortLevels: undefined,
|
|
1394
1585
|
version: 0,
|
|
@@ -1407,6 +1598,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1407
1598
|
return ns?.value?.recapOnOpen !== false;
|
|
1408
1599
|
},
|
|
1409
1600
|
agentId: agent.id,
|
|
1601
|
+
agentBindingGeneration: 0,
|
|
1410
1602
|
model: options.model,
|
|
1411
1603
|
provider: options.provider,
|
|
1412
1604
|
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, peak: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, idle: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },
|
|
@@ -1442,6 +1634,8 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1442
1634
|
scrollGutter: normalizeScrollGutter(options.scrollGutter),
|
|
1443
1635
|
foldTerminalCommand: options.foldTerminalCommand === true,
|
|
1444
1636
|
promptSessionLabel: options.promptSessionLabel === true,
|
|
1637
|
+
expandEditor: options.expandEditor !== false,
|
|
1638
|
+
smoothStreaming: options.smoothStreaming !== false,
|
|
1445
1639
|
statusBar: normalizeStatusBar(options.statusBar),
|
|
1446
1640
|
whale: options.whale !== false,
|
|
1447
1641
|
minimal: options.minimal === true,
|
|
@@ -1484,21 +1678,34 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1484
1678
|
];
|
|
1485
1679
|
}
|
|
1486
1680
|
if (path.length === 1 && path[0] === 'theme') {
|
|
1681
|
+
const themeEntries = listThemeCatalog(themeHost);
|
|
1487
1682
|
return [
|
|
1488
1683
|
{ name: 'status', description: 'Show the current theme', descriptionKey: 'sugg-status-desc' },
|
|
1489
1684
|
{ name: AUTO_THEME_NAME, description: 'Follow the terminal background', descriptionKey: 'sugg-theme-auto-desc' },
|
|
1490
|
-
...
|
|
1491
|
-
name
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1685
|
+
...themeEntries
|
|
1686
|
+
.filter((entry) => entry.name !== AUTO_THEME_NAME)
|
|
1687
|
+
.map((entry) => {
|
|
1688
|
+
const base = entry.base ?? 'dark';
|
|
1689
|
+
if (entry.source === 'builtin') {
|
|
1690
|
+
return {
|
|
1691
|
+
name: entry.name,
|
|
1692
|
+
description: `Built-in theme ${entry.name}`,
|
|
1693
|
+
descriptionKey: 'sugg-theme-builtin-desc',
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
if (entry.source === 'runtime') {
|
|
1697
|
+
return {
|
|
1698
|
+
name: entry.name,
|
|
1699
|
+
description: `Plugin theme (${base} base)`,
|
|
1700
|
+
descriptionKey: 'sugg-theme-plugin-desc',
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
return {
|
|
1704
|
+
name: entry.name,
|
|
1705
|
+
description: `User theme (${base} base)`,
|
|
1706
|
+
descriptionKey: 'sugg-theme-user-desc',
|
|
1707
|
+
};
|
|
1708
|
+
}),
|
|
1502
1709
|
];
|
|
1503
1710
|
}
|
|
1504
1711
|
if (path.length === 1 && path[0] === 'color') {
|
|
@@ -1573,13 +1780,23 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1573
1780
|
];
|
|
1574
1781
|
}
|
|
1575
1782
|
if (path.length === 1 && path[0] === 'permission') {
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1783
|
+
const snapshot = state.permissionPresets();
|
|
1784
|
+
return snapshot.options
|
|
1785
|
+
.filter(option => isCommandCompletionToken(option.value))
|
|
1786
|
+
.map(option => ({
|
|
1787
|
+
name: option.value,
|
|
1788
|
+
description: option.description ?? option.name,
|
|
1789
|
+
...(option.value === 'read-only'
|
|
1790
|
+
? { descriptionKey: 'permission-preset-readonly-desc' }
|
|
1791
|
+
: option.value === 'workspace-write'
|
|
1792
|
+
? { descriptionKey: 'permission-preset-workspace-write-desc' }
|
|
1793
|
+
: option.value === 'danger-full-access'
|
|
1794
|
+
? { descriptionKey: 'permission-preset-full-access-desc' }
|
|
1795
|
+
: {}),
|
|
1796
|
+
...(snapshot.current?.kind === 'preset' && snapshot.current.value === option.value
|
|
1797
|
+
? { tag: 'current' }
|
|
1798
|
+
: {}),
|
|
1799
|
+
}));
|
|
1583
1800
|
}
|
|
1584
1801
|
if (path.length === 1 && path[0] === 'plan') {
|
|
1585
1802
|
return [
|
|
@@ -1833,6 +2050,10 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1833
2050
|
return null;
|
|
1834
2051
|
}
|
|
1835
2052
|
}
|
|
2053
|
+
// An in-flight manual compaction must not straddle the fork: cancel it
|
|
2054
|
+
// and wait, or its checkpoint could commit right after the seed snapshot
|
|
2055
|
+
// below and quietly replace history the rewind was meant to preserve.
|
|
2056
|
+
await settleManualCompaction();
|
|
1836
2057
|
const childId = SessionId(randomUUID());
|
|
1837
2058
|
// DSH event order is `turn/start → user/message → … → turn/end`, so a
|
|
1838
2059
|
// message's own seq always sits inside its turn — forking there would
|
|
@@ -2363,6 +2584,10 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2363
2584
|
state.notify(t('rewind-unavailable'), { color: 'error' });
|
|
2364
2585
|
return null;
|
|
2365
2586
|
}
|
|
2587
|
+
// An in-flight manual compaction must not straddle the snapshot below
|
|
2588
|
+
// (live branch) nor keep summarizing the current session while the
|
|
2589
|
+
// rewind targets another — cancel and await it first.
|
|
2590
|
+
await settleManualCompaction();
|
|
2366
2591
|
// Pin the entry-time session: the awaits below (log load, preset
|
|
2367
2592
|
// compose, agent create) are windows in which a queued switch
|
|
2368
2593
|
// (/new, /resume, /model) can swap `agent` — the mutation queue only
|
|
@@ -2543,6 +2768,10 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2543
2768
|
state.notify(t('fork-while-working'), { color: 'warning' });
|
|
2544
2769
|
return false;
|
|
2545
2770
|
}
|
|
2771
|
+
// An in-flight manual compaction must not straddle the fork snapshot:
|
|
2772
|
+
// cancel and await it, or its checkpoint could commit right after the
|
|
2773
|
+
// seed copy below and quietly replace history the fork preserved.
|
|
2774
|
+
await settleManualCompaction();
|
|
2546
2775
|
const source = agent.session;
|
|
2547
2776
|
const childId = SessionId(randomUUID());
|
|
2548
2777
|
// No boundary: the whole (turn-closed) log. Slice via sessions.fork for
|
|
@@ -2641,6 +2870,15 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2641
2870
|
// untouched.
|
|
2642
2871
|
if (await sessionSwitchVetoed('resume', sessionId))
|
|
2643
2872
|
return { ok: false, reason: 'cancelled' };
|
|
2873
|
+
// The live session's in-flight manual compaction must not keep running
|
|
2874
|
+
// (and commit its checkpoint) once we leave it for the target — cancel
|
|
2875
|
+
// and await it before any target read.
|
|
2876
|
+
await settleManualCompaction();
|
|
2877
|
+
// Identity pin for the rival-swap guard below: everything between here
|
|
2878
|
+
// and the adoption can await (veto, preset, route, agents.resume), and
|
|
2879
|
+
// an interrupt-queued /new or a second /resume may commit a different
|
|
2880
|
+
// swap in that window.
|
|
2881
|
+
const entrySession = agent.session;
|
|
2644
2882
|
let handle;
|
|
2645
2883
|
// Compat boundary: register vouched-for legacy event types (e.g.
|
|
2646
2884
|
// activity/status from pre-#143 logs) in every reachable dsh-session
|
|
@@ -2692,6 +2930,17 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2692
2930
|
catch (error) {
|
|
2693
2931
|
state.notify(t('resume-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
|
|
2694
2932
|
}
|
|
2933
|
+
// Rival-swap guard (rewindToNode's entrySession check, applied to the
|
|
2934
|
+
// resume path): the awaits above can straddle another session swap
|
|
2935
|
+
// committing first, and adopting now would stomp the newer session's
|
|
2936
|
+
// live transcript with this target's replay. Free the just-created
|
|
2937
|
+
// handle and bail — the live session stays exactly as the rival left
|
|
2938
|
+
// it, and the persisted target simply stays in /resume.
|
|
2939
|
+
if (agent.session !== entrySession) {
|
|
2940
|
+
void handle.dispose().catch(() => { });
|
|
2941
|
+
state.notify(t('resume-session-changed'), { color: 'error' });
|
|
2942
|
+
return { ok: false, reason: 'failed', error: 'live session changed during resume' };
|
|
2943
|
+
}
|
|
2695
2944
|
// Replay the persisted history into a fresh transcript (same reset as
|
|
2696
2945
|
// rewindTo, plus the context window which the replay re-derives).
|
|
2697
2946
|
streaming = undefined;
|
|
@@ -2703,6 +2952,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2703
2952
|
toolCards.clear();
|
|
2704
2953
|
nextRowId = 0;
|
|
2705
2954
|
state.rows.length = 0;
|
|
2955
|
+
resetSubagentProjection();
|
|
2706
2956
|
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
2707
2957
|
// the session being entered (or leaves them empty).
|
|
2708
2958
|
state.todos = [];
|
|
@@ -2805,12 +3055,20 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2805
3055
|
// happened yet — the session id below is not even allocated.
|
|
2806
3056
|
if (await sessionSwitchVetoed('new'))
|
|
2807
3057
|
return false;
|
|
3058
|
+
// Leaving the live session: its in-flight manual compaction must not
|
|
3059
|
+
// keep summarizing (and later commit a checkpoint the user believes
|
|
3060
|
+
// cancelled) — cancel and await it first.
|
|
3061
|
+
await settleManualCompaction();
|
|
2808
3062
|
const sessionId = SessionId(randomUUID());
|
|
2809
3063
|
let handle;
|
|
2810
3064
|
// A fresh session composes the caller's DEFAULT preset: the cordis.yml
|
|
2811
3065
|
// `preset` key wins over the persisted `/preset` choice, which wins
|
|
2812
3066
|
// over the roster default (same precedence as activityFrames).
|
|
2813
|
-
const
|
|
3067
|
+
const presetPref = options.configuredPreset === undefined ? readPresetPref() : undefined;
|
|
3068
|
+
const newComposed = await composePreset(ctx, options.configuredPreset ?? presetPref);
|
|
3069
|
+
if (!migratePresetPref(presetPref, newComposed.agentPreset)) {
|
|
3070
|
+
state.notify(t('preset-switched-pref-failed', { id: newComposed.agentPreset ?? presetPref ?? 'unknown' }), { color: 'warning' });
|
|
3071
|
+
}
|
|
2814
3072
|
// Same precedence for the route (issues #14/#30/#67): the pair resolves
|
|
2815
3073
|
// atomically — a complete cordis.yml route wins whole, else the
|
|
2816
3074
|
// persisted `/model` choice (a switch earlier in this run just wrote
|
|
@@ -2872,6 +3130,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2872
3130
|
lastTextDelta.clear();
|
|
2873
3131
|
nextRowId = 0;
|
|
2874
3132
|
state.rows.length = 0;
|
|
3133
|
+
resetSubagentProjection();
|
|
2875
3134
|
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
2876
3135
|
// the session being entered (or leaves them empty).
|
|
2877
3136
|
state.todos = [];
|
|
@@ -3008,6 +3267,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
3008
3267
|
}
|
|
3009
3268
|
let seed;
|
|
3010
3269
|
try {
|
|
3270
|
+
// An in-flight manual compaction must not straddle the fork: cancel
|
|
3271
|
+
// it first, or its checkpoint can commit right after this snapshot —
|
|
3272
|
+
// the model-switched child would start from the summary alone while
|
|
3273
|
+
// the user believes the full history carried over ("context lost").
|
|
3274
|
+
await settleManualCompaction();
|
|
3011
3275
|
// No boundary = fork the whole log (continue the conversation).
|
|
3012
3276
|
seed = sessions.fork(agent.session).events;
|
|
3013
3277
|
}
|
|
@@ -3057,6 +3321,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
3057
3321
|
toolCards.clear();
|
|
3058
3322
|
nextRowId = 0;
|
|
3059
3323
|
state.rows.length = 0;
|
|
3324
|
+
resetSubagentProjection();
|
|
3060
3325
|
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
3061
3326
|
// the session being entered (or leaves them empty).
|
|
3062
3327
|
state.todos = [];
|
|
@@ -3111,7 +3376,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
3111
3376
|
currentHandle = handle;
|
|
3112
3377
|
bindAgent();
|
|
3113
3378
|
// Model-switch quip rides the fresh tracker (pi parity).
|
|
3114
|
-
activityTracker.onModelSwitch(model);
|
|
3379
|
+
updateWorkingActivity('model switch', () => activityTracker.onModelSwitch(model));
|
|
3115
3380
|
refreshCommandList();
|
|
3116
3381
|
void refreshLoadedContext();
|
|
3117
3382
|
void refreshSkillCommands();
|
|
@@ -3119,6 +3384,9 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
3119
3384
|
touchSession(childId);
|
|
3120
3385
|
state.emit();
|
|
3121
3386
|
void oldHandle?.dispose().catch(() => { });
|
|
3387
|
+
// Staged image tokens were typed against the pre-switch conversation;
|
|
3388
|
+
// resumeTo/newSession already drop theirs on the swap — same contract.
|
|
3389
|
+
clearStagedImages();
|
|
3122
3390
|
// Persist the choice so the next boot and `/new` start on it (same
|
|
3123
3391
|
// contract as /preset and /effort; issues #14/#30). A failed
|
|
3124
3392
|
// write keeps the live switch but warns it will not survive a restart.
|
|
@@ -3138,6 +3406,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
3138
3406
|
streaming = undefined;
|
|
3139
3407
|
reasoning = undefined;
|
|
3140
3408
|
toolCards.clear();
|
|
3409
|
+
// In-flight subagents keep streaming after the wipe; clearing the row
|
|
3410
|
+
// map lets their next event re-create the card as a fresh row instead
|
|
3411
|
+
// of feeding a row object no transcript holds (the store keeps live
|
|
3412
|
+
// tracking for the dashboard — same session, still running).
|
|
3413
|
+
dropSubagentRows();
|
|
3141
3414
|
state.activeToolCount = 0;
|
|
3142
3415
|
state.responseChars = 0;
|
|
3143
3416
|
state.rows.push({
|
|
@@ -3214,6 +3487,18 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
3214
3487
|
state.promptSessionLabel = enabled;
|
|
3215
3488
|
state.emit();
|
|
3216
3489
|
},
|
|
3490
|
+
setExpandEditor(enabled) {
|
|
3491
|
+
if (enabled === state.expandEditor)
|
|
3492
|
+
return;
|
|
3493
|
+
state.expandEditor = enabled;
|
|
3494
|
+
state.emit();
|
|
3495
|
+
},
|
|
3496
|
+
setSmoothStreaming(enabled) {
|
|
3497
|
+
if (enabled === state.smoothStreaming)
|
|
3498
|
+
return;
|
|
3499
|
+
state.smoothStreaming = enabled;
|
|
3500
|
+
state.emit();
|
|
3501
|
+
},
|
|
3217
3502
|
setStatusBar(config) {
|
|
3218
3503
|
const next = normalizeStatusBar({ ...state.statusBar, ...config });
|
|
3219
3504
|
const changed = Object.keys(next).some(key => next[key] !== state.statusBar[key]);
|
|
@@ -3255,6 +3540,18 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
3255
3540
|
state.notify(t('activity-indicator-switched', { name }));
|
|
3256
3541
|
return true;
|
|
3257
3542
|
},
|
|
3543
|
+
permissionPresets() {
|
|
3544
|
+
let service;
|
|
3545
|
+
try {
|
|
3546
|
+
service = ctx.get('permissionPresets');
|
|
3547
|
+
}
|
|
3548
|
+
catch {
|
|
3549
|
+
return unavailablePermissionPresetSnapshot();
|
|
3550
|
+
}
|
|
3551
|
+
if (service === undefined)
|
|
3552
|
+
return legacyPermissionPresetSnapshot(state.mode.sandbox);
|
|
3553
|
+
return permissionPresetSnapshotFromService(service, agent.session.events);
|
|
3554
|
+
},
|
|
3258
3555
|
async listPresets() {
|
|
3259
3556
|
const presets = rosterOf(ctx);
|
|
3260
3557
|
if (presets === undefined)
|
|
@@ -3285,17 +3582,21 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
3285
3582
|
}
|
|
3286
3583
|
let target;
|
|
3287
3584
|
try {
|
|
3288
|
-
target = await presets
|
|
3585
|
+
target = await resolveCompatiblePreset(presets, presetId);
|
|
3289
3586
|
}
|
|
3290
3587
|
catch (error) {
|
|
3291
3588
|
state.notify(t('preset-not-found', { id: presetId, err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
3292
3589
|
return false;
|
|
3293
3590
|
}
|
|
3294
3591
|
if (target.broken !== undefined) {
|
|
3295
|
-
state.notify(t('preset-load-failed', { id:
|
|
3592
|
+
state.notify(t('preset-load-failed', { id: target.id, broken: target.broken }), { color: 'error', timeoutMs: 8000 });
|
|
3296
3593
|
return false;
|
|
3297
3594
|
}
|
|
3298
3595
|
if (target.id === state.agentPreset) {
|
|
3596
|
+
if (!migratePresetPref(presetId, target.id)) {
|
|
3597
|
+
state.notify(t('preset-switched-pref-failed', { id: target.id }), { color: 'warning' });
|
|
3598
|
+
return true;
|
|
3599
|
+
}
|
|
3299
3600
|
state.notify(t('preset-already-current', { id: target.id }), { color: 'success' });
|
|
3300
3601
|
return true;
|
|
3301
3602
|
}
|
|
@@ -3436,6 +3737,14 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
3436
3737
|
async writeCredential(ref, value) {
|
|
3437
3738
|
if (!credentials)
|
|
3438
3739
|
throw new Error('credentials service unavailable');
|
|
3740
|
+
// Second layer of the secret-ref reservation guard: the
|
|
3741
|
+
// registration layer already rejects plugin sections with
|
|
3742
|
+
// host-owned refs, but this seam must not trust it — a stale
|
|
3743
|
+
// section (registered before the guard) or a direct call must not
|
|
3744
|
+
// reach the shared credentials. The host's own main-credential
|
|
3745
|
+
// writes go through providerSetup().writeCredential instead.
|
|
3746
|
+
if (isReservedCredentialRef(ref))
|
|
3747
|
+
throw new Error(t('settings-secret-ref-reserved', { ref }));
|
|
3439
3748
|
await credentials.set(ref, value);
|
|
3440
3749
|
},
|
|
3441
3750
|
};
|
|
@@ -3751,18 +4060,43 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
3751
4060
|
state.notify(t('compact-while-working'), { color: 'warning' });
|
|
3752
4061
|
return;
|
|
3753
4062
|
}
|
|
3754
|
-
const
|
|
4063
|
+
const controller = new AbortController();
|
|
3755
4064
|
state.notify(t('compact-working'));
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
4065
|
+
// Register the in-flight transaction so any agent-replacing path
|
|
4066
|
+
// (rewind/resume/new/model switch) can cancel it before snapshotting
|
|
4067
|
+
// the session — see settleManualCompaction. `settled` never rejects:
|
|
4068
|
+
// every branch lands in a notification.
|
|
4069
|
+
const settled = (async () => {
|
|
4070
|
+
try {
|
|
4071
|
+
const result = await compactService.compactNow(agent, controller.signal);
|
|
4072
|
+
state.notify(result ? t('compact-done') : t('compact-nothing'));
|
|
4073
|
+
// Compaction quip rides the next thinking rotation (pi parity).
|
|
4074
|
+
if (result)
|
|
4075
|
+
updateWorkingActivity('compaction', () => activityTracker.onCompact('done'));
|
|
4076
|
+
}
|
|
4077
|
+
catch (error) {
|
|
4078
|
+
// ManualCompactionError('persistence'): the replacement checkpoint
|
|
4079
|
+
// is ALREADY committed — only the durability flush failed. The
|
|
4080
|
+
// surface is now the summary, so a plain "failed" toast here sent
|
|
4081
|
+
// users to /model expecting full history and finding only the
|
|
4082
|
+
// summary ("context lost"). Distinguish it, structurally — the
|
|
4083
|
+
// TUI must not import the error class across the adapter seam.
|
|
4084
|
+
if (error.code === 'persistence') {
|
|
4085
|
+
state.notify(t('compact-flush-failed'), { color: 'warning', timeoutMs: 12000 });
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
// A switch-initiated abort rejects compactNow with the abort reason;
|
|
4089
|
+
// the cancellation was already toasted above — a second generic
|
|
4090
|
+
// "failed" toast for the same, expected rejection would mislead.
|
|
4091
|
+
if (cancelledCompactions.has(controller))
|
|
4092
|
+
return;
|
|
4093
|
+
state.notify(t('compact-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
4094
|
+
}
|
|
4095
|
+
})();
|
|
4096
|
+
manualCompaction = { controller, settled };
|
|
4097
|
+
void settled.finally(() => {
|
|
4098
|
+
if (manualCompaction?.controller === controller)
|
|
4099
|
+
manualCompaction = undefined;
|
|
3766
4100
|
});
|
|
3767
4101
|
})().catch((error) => {
|
|
3768
4102
|
// Sync throws from compactNow (e.g. runMaintenance rejecting a
|
|
@@ -4039,19 +4373,21 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
4039
4373
|
...(renderedInstructions?.truncated ?? []).map(file => file.displayPath),
|
|
4040
4374
|
]);
|
|
4041
4375
|
files.push(...[...instructionPaths].map(displayPath => ({ displayPath })));
|
|
4042
|
-
//
|
|
4043
|
-
//
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
if (
|
|
4048
|
-
const
|
|
4376
|
+
// A registry entry reaches the model only through dsh-tool-skill's
|
|
4377
|
+
// catalog, which is gated on that exact tool being visible to the agent.
|
|
4378
|
+
const skillsRegistry = tools.some(tool => tool.name === 'skill')
|
|
4379
|
+
? skillRegistryFor(target)
|
|
4380
|
+
: undefined;
|
|
4381
|
+
if (skillsRegistry !== undefined) {
|
|
4382
|
+
const observation = await skillsRegistry.snapshot(skillViewOptions(target));
|
|
4049
4383
|
if (target !== agent)
|
|
4050
4384
|
return;
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4385
|
+
if (observation.complete) {
|
|
4386
|
+
skills.push(...observation.skills.filter(isModelInvocable).map(skill => ({
|
|
4387
|
+
name: skill.name,
|
|
4388
|
+
description: skill.description,
|
|
4389
|
+
})));
|
|
4390
|
+
}
|
|
4055
4391
|
}
|
|
4056
4392
|
}
|
|
4057
4393
|
catch (error) {
|
|
@@ -4065,7 +4401,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
4065
4401
|
* Rebuild the merged slash-command list: built-in locals, then registry
|
|
4066
4402
|
* commands (plan/goal/…), then user-invocable skills from the DSH skill
|
|
4067
4403
|
* registry (issue #86 — filesystem-discovered skills must appear in the
|
|
4068
|
-
* `/` menu and Tab completion, like /
|
|
4404
|
+
* `/` menu and Tab completion, like /my-skill). Skill entries
|
|
4069
4405
|
* are completion-only: dispatch falls through to the model as plain text,
|
|
4070
4406
|
* where dsh-tool-skill's pre-step hook injects the skill body — the same
|
|
4071
4407
|
* path a hand-typed `/skill-name` takes. Registry and skill reads are
|
|
@@ -4538,7 +4874,7 @@ ${output}
|
|
|
4538
4874
|
streaming = existing;
|
|
4539
4875
|
return existing;
|
|
4540
4876
|
}
|
|
4541
|
-
streaming = { id: nextRowId, kind: 'assistant', text: '', streaming: true, ...seq !== undefined ? { seq } : {} };
|
|
4877
|
+
streaming = { id: nextRowId, kind: 'assistant', text: '', streaming: true, fresh: true, ...seq !== undefined ? { seq } : {} };
|
|
4542
4878
|
nextRowId += 1;
|
|
4543
4879
|
state.rows.push(streaming);
|
|
4544
4880
|
return streaming;
|
|
@@ -4877,6 +5213,11 @@ ${output}
|
|
|
4877
5213
|
if (text)
|
|
4878
5214
|
row.text = text;
|
|
4879
5215
|
row.streaming = false;
|
|
5216
|
+
// Live settles keep the smooth-reveal cursor alive (a one-shot
|
|
5217
|
+
// non-streaming delivery still paints as a flow); replayed
|
|
5218
|
+
// settles must not — the transcript would typewrite on open.
|
|
5219
|
+
if (!replaying && text)
|
|
5220
|
+
row.fresh = true;
|
|
4880
5221
|
}
|
|
4881
5222
|
streaming = undefined;
|
|
4882
5223
|
if (reasoning !== undefined) {
|
|
@@ -5000,6 +5341,9 @@ ${output}
|
|
|
5000
5341
|
kind: 'tool',
|
|
5001
5342
|
text: '',
|
|
5002
5343
|
seq: event.seq,
|
|
5344
|
+
// Smooth-reveal participation flag: live cards animate their body
|
|
5345
|
+
// in; replayed cards (resume/rewind) paint complete.
|
|
5346
|
+
fresh: !replaying,
|
|
5003
5347
|
tool: {
|
|
5004
5348
|
callId: event.data.callId,
|
|
5005
5349
|
name: event.data.name,
|
|
@@ -5153,21 +5497,31 @@ ${output}
|
|
|
5153
5497
|
case 'session/title':
|
|
5154
5498
|
state.sessionTitle = event.data.title;
|
|
5155
5499
|
break;
|
|
5156
|
-
case 'todo/write':
|
|
5157
|
-
// Whole-list snapshot — latest write wins; log-only UI state.
|
|
5158
|
-
state.todos = event.data.todos;
|
|
5159
|
-
break;
|
|
5160
5500
|
default:
|
|
5501
|
+
// dsh-tool-todo owns this optional module augmentation in alpha.2.
|
|
5502
|
+
// Match by name so the TUI remains loadable without that plugin.
|
|
5503
|
+
if (event.type === 'todo/write') {
|
|
5504
|
+
const todos = todoPanelItems(event.data);
|
|
5505
|
+
if (todos !== undefined)
|
|
5506
|
+
state.todos = todos;
|
|
5507
|
+
break;
|
|
5508
|
+
}
|
|
5161
5509
|
// Logged preset switch (blank sessions only, issue #8): a transcript
|
|
5162
5510
|
// marker so a replayed log shows which composition produced the
|
|
5163
5511
|
// turns after it. Not in dsh-session's typed union — matched here by
|
|
5164
5512
|
// name, like the other plugin-defined events above.
|
|
5165
5513
|
if (event.type === 'agent-preset/selected') {
|
|
5166
5514
|
const data = event.data;
|
|
5515
|
+
const recordedPreset = typeof data.agentPreset === 'string' ? data.agentPreset : undefined;
|
|
5516
|
+
const renamedOfficialPreset = (recordedPreset === 'code' && state.agentPreset === 'ptc') ||
|
|
5517
|
+
(recordedPreset === 'ptc' && state.agentPreset === 'code');
|
|
5518
|
+
const preset = renamedOfficialPreset && state.agentPreset !== undefined
|
|
5519
|
+
? state.agentPreset
|
|
5520
|
+
: recordedPreset ?? 'unknown';
|
|
5167
5521
|
state.rows.push({
|
|
5168
5522
|
id: nextRowId,
|
|
5169
5523
|
kind: 'notice',
|
|
5170
|
-
text: t('agent-preset-switched', { preset
|
|
5524
|
+
text: t('agent-preset-switched', { preset }),
|
|
5171
5525
|
});
|
|
5172
5526
|
nextRowId += 1;
|
|
5173
5527
|
break;
|
|
@@ -5264,17 +5618,60 @@ ${output}
|
|
|
5264
5618
|
state.workingActivity = rendered;
|
|
5265
5619
|
return rendered;
|
|
5266
5620
|
};
|
|
5621
|
+
// Working Activity is an optional presentation sidecar. A malformed durable
|
|
5622
|
+
// event must never let it abort the authoritative channel projection (Cordis
|
|
5623
|
+
// contains the listener throw, but the rest of THIS callback would otherwise
|
|
5624
|
+
// be skipped — including turn/end and inbox retirement).
|
|
5625
|
+
let activityFailureReported = false;
|
|
5626
|
+
const updateWorkingActivity = (source, update) => {
|
|
5627
|
+
try {
|
|
5628
|
+
update?.();
|
|
5629
|
+
return renderWorkingActivity();
|
|
5630
|
+
}
|
|
5631
|
+
catch (error) {
|
|
5632
|
+
if (!activityFailureReported) {
|
|
5633
|
+
activityFailureReported = true;
|
|
5634
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
5635
|
+
ctx.logger.warn(`dsh-tui: working-activity ignored ${source} after a projection error: ${detail}`);
|
|
5636
|
+
}
|
|
5637
|
+
return undefined;
|
|
5638
|
+
}
|
|
5639
|
+
};
|
|
5640
|
+
/**
|
|
5641
|
+
* Release volatile UI gates when the bound driver is definitively quiescent
|
|
5642
|
+
* but its terminal session event did not reach this projection. This does not
|
|
5643
|
+
* invent a turn/end or any transcript fact; it only reconciles live controls
|
|
5644
|
+
* to the authoritative Agent status so Enter/Esc cannot remain latched.
|
|
5645
|
+
*/
|
|
5646
|
+
const reconcileRetiredProjection = (status) => {
|
|
5647
|
+
if (!state.working)
|
|
5648
|
+
return;
|
|
5649
|
+
ctx.logger.warn(`dsh-tui: agent became ${status} while the channel still projected an open turn; releasing volatile UI gates`);
|
|
5650
|
+
cancelInFlight = false;
|
|
5651
|
+
state.cancelPending = false;
|
|
5652
|
+
state.working = false;
|
|
5653
|
+
state.activeToolCount = 0;
|
|
5654
|
+
settleStreaming();
|
|
5655
|
+
updateSpinnerMode();
|
|
5656
|
+
};
|
|
5267
5657
|
const bindAgent = () => {
|
|
5658
|
+
agentBindingGeneration += 1;
|
|
5659
|
+
state.agentBindingGeneration = agentBindingGeneration;
|
|
5268
5660
|
for (const dispose of agentSubscriptions)
|
|
5269
5661
|
dispose();
|
|
5270
5662
|
stopActivityTick();
|
|
5663
|
+
// Cancel state and deferred interrupt delivery belong to one bound agent.
|
|
5664
|
+
// A replacement must neither inherit the old latch nor receive its queued
|
|
5665
|
+
// microtask after the session identity changes.
|
|
5666
|
+
cancelInFlight = false;
|
|
5667
|
+
interruptSeq += 1;
|
|
5271
5668
|
const prefs = activityPrefsSnapshot();
|
|
5272
5669
|
activityTracker = new ActivityTracker(prefs.config, Date.now, prefs.customActions);
|
|
5273
|
-
|
|
5274
|
-
|
|
5670
|
+
activityFailureReported = false;
|
|
5671
|
+
updateWorkingActivity('agent bind', () => activityTracker.onAgentStatus(agent.status));
|
|
5275
5672
|
activityTickTimer = setInterval(() => {
|
|
5276
5673
|
const previous = state.workingActivity;
|
|
5277
|
-
const rendered =
|
|
5674
|
+
const rendered = updateWorkingActivity('activity tick');
|
|
5278
5675
|
if (rendered === undefined)
|
|
5279
5676
|
return;
|
|
5280
5677
|
// Live phases deliberately wake at 500 ms even when the formatted line
|
|
@@ -5318,8 +5715,9 @@ ${output}
|
|
|
5318
5715
|
if (subject !== agent)
|
|
5319
5716
|
return;
|
|
5320
5717
|
state.status = status;
|
|
5321
|
-
activityTracker.onAgentStatus(status);
|
|
5322
|
-
|
|
5718
|
+
updateWorkingActivity(`agent/status:${status}`, () => activityTracker.onAgentStatus(status));
|
|
5719
|
+
if (status === 'idle')
|
|
5720
|
+
reconcileRetiredProjection('idle');
|
|
5323
5721
|
state.emit();
|
|
5324
5722
|
}),
|
|
5325
5723
|
ctx.on('agent/disposed', ({ agent: subject }) => {
|
|
@@ -5327,6 +5725,7 @@ ${output}
|
|
|
5327
5725
|
return;
|
|
5328
5726
|
state.status = 'disposed';
|
|
5329
5727
|
stopActivityTick();
|
|
5728
|
+
reconcileRetiredProjection('disposed');
|
|
5330
5729
|
state.emit();
|
|
5331
5730
|
}),
|
|
5332
5731
|
// Pending delivery is driven by the agent inbox: a claimed message
|
|
@@ -5358,9 +5757,16 @@ ${output}
|
|
|
5358
5757
|
};
|
|
5359
5758
|
})(),
|
|
5360
5759
|
ctx.on('session/event', (session, event) => {
|
|
5361
|
-
//
|
|
5362
|
-
|
|
5363
|
-
if
|
|
5760
|
+
// The currently bound main session always wins. SubagentActivityStore
|
|
5761
|
+
// intentionally retains Session-object mappings for completed cards;
|
|
5762
|
+
// if one of those sessions is later adopted/resumed as the main agent,
|
|
5763
|
+
// checking the stale child mapping first would swallow every main event
|
|
5764
|
+
// (including turn/end) and leave working/cancelPending latched forever.
|
|
5765
|
+
const isMainSession = session === agent.session;
|
|
5766
|
+
const subagentId = isMainSession
|
|
5767
|
+
? undefined
|
|
5768
|
+
: subagentStore.getSubagentIdBySession(session);
|
|
5769
|
+
if (subagentId !== undefined) {
|
|
5364
5770
|
subagentStore.onSessionEvent(subagentId, event);
|
|
5365
5771
|
if (event.type === 'assistant/chunk') {
|
|
5366
5772
|
// Token-rate path (100-300 events/s): the store append stays
|
|
@@ -5376,23 +5782,24 @@ ${output}
|
|
|
5376
5782
|
}
|
|
5377
5783
|
return;
|
|
5378
5784
|
}
|
|
5379
|
-
// Otherwise handle main
|
|
5380
|
-
if (
|
|
5785
|
+
// Otherwise handle the bound main-agent session.
|
|
5786
|
+
if (!isMainSession)
|
|
5381
5787
|
return;
|
|
5382
5788
|
// Observation broker (C-042): maps user/message + assistant/message
|
|
5383
5789
|
// into grant-gated envelopes; every other event type is a no-op, and
|
|
5384
5790
|
// publish never throws into this arm.
|
|
5385
5791
|
messageObserver?.publish(session, event);
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5792
|
+
updateWorkingActivity(`session/event:${event.type}`, () => {
|
|
5793
|
+
activityTracker.onSessionEvent(event);
|
|
5794
|
+
// Interrupt quip: an aborted/interrupted turn ends the round; the
|
|
5795
|
+
// comeback copy shows on the next thinking rotation (pi parity).
|
|
5796
|
+
if (event.type === 'turn/end') {
|
|
5797
|
+
const reason = event.data.reason;
|
|
5798
|
+
if (reason?.kind === 'aborted' || reason?.kind === 'interrupted') {
|
|
5799
|
+
activityTracker.onInterrupted();
|
|
5800
|
+
}
|
|
5393
5801
|
}
|
|
5394
|
-
}
|
|
5395
|
-
renderWorkingActivity();
|
|
5802
|
+
});
|
|
5396
5803
|
// Mode-affecting atoms fold into the Shift+Tab mode indicator the
|
|
5397
5804
|
// moment they land (whether appended by cycleMode or by hand).
|
|
5398
5805
|
const eventType = event.type;
|
|
@@ -5519,7 +5926,7 @@ ${output}
|
|
|
5519
5926
|
// is exactly what the column claims.
|
|
5520
5927
|
noteBranch(agent.session.id, branch);
|
|
5521
5928
|
// Feed the working line so git tools can show ` · git <branch>`.
|
|
5522
|
-
activityTracker.onGitBranch(branch);
|
|
5929
|
+
updateWorkingActivity('git branch', () => activityTracker.onGitBranch(branch));
|
|
5523
5930
|
state.emit();
|
|
5524
5931
|
}
|
|
5525
5932
|
})
|
|
@@ -5591,6 +5998,18 @@ export function sessionCwdMatches(stateCwd, headerCwd, caseInsensitive = process
|
|
|
5591
5998
|
function estimateTokens(text) {
|
|
5592
5999
|
return Math.ceil(text.length / 4);
|
|
5593
6000
|
}
|
|
6001
|
+
/** Whether one stream chunk advances the first-token/decode boundary. */
|
|
6002
|
+
function isTokenDelta(chunk) {
|
|
6003
|
+
switch (chunk.type) {
|
|
6004
|
+
case 'text-delta':
|
|
6005
|
+
case 'reasoning-delta':
|
|
6006
|
+
return chunk.text !== '';
|
|
6007
|
+
case 'tool-call-delta':
|
|
6008
|
+
return chunk.argumentsDelta !== '' || chunk.name !== undefined;
|
|
6009
|
+
default:
|
|
6010
|
+
return false;
|
|
6011
|
+
}
|
|
6012
|
+
}
|
|
5594
6013
|
/** Character payload of one token-bearing stream delta for the live fallback. */
|
|
5595
6014
|
function tokenDeltaChars(chunk) {
|
|
5596
6015
|
switch (chunk.type) {
|