@deepseek-harness-tui/dsh-tui 0.8.7 → 0.9.0
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 +29 -10
- package/bin/dsh-tui.js +1 -1
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +4 -2
- package/lib/types/components/ActivityPicker.d.ts +4 -1
- package/lib/types/components/ActivityPicker.d.ts.map +1 -1
- package/lib/types/components/ActivityPicker.js +2 -2
- package/lib/types/components/BtwPanel.d.ts.map +1 -1
- package/lib/types/components/BtwPanel.js +1 -1
- package/lib/types/components/Chat/SubagentMessage.d.ts +2 -1
- package/lib/types/components/Chat/SubagentMessage.d.ts.map +1 -1
- package/lib/types/components/Chat/SubagentMessage.js +11 -3
- package/lib/types/components/CommandSuggestions.d.ts +21 -4
- package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
- package/lib/types/components/CommandSuggestions.js +41 -14
- package/lib/types/components/EffortSlider.d.ts +4 -1
- package/lib/types/components/EffortSlider.d.ts.map +1 -1
- package/lib/types/components/EffortSlider.js +4 -2
- package/lib/types/components/ExtensionDialog.js +6 -2
- package/lib/types/components/FileSuggestions.d.ts +26 -5
- package/lib/types/components/FileSuggestions.d.ts.map +1 -1
- package/lib/types/components/FileSuggestions.js +53 -16
- package/lib/types/components/GoalTodoPanel.d.ts +26 -2
- package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
- package/lib/types/components/GoalTodoPanel.js +86 -17
- package/lib/types/components/HelpMenu.d.ts +7 -1
- package/lib/types/components/HelpMenu.d.ts.map +1 -1
- package/lib/types/components/HelpMenu.js +12 -4
- package/lib/types/components/HistorySearchDialog.d.ts +4 -1
- package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
- package/lib/types/components/HistorySearchDialog.js +2 -2
- package/lib/types/components/LangPicker.d.ts +14 -0
- package/lib/types/components/LangPicker.d.ts.map +1 -0
- package/lib/types/components/LangPicker.js +17 -0
- package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
- package/lib/types/components/LoadedContextPanel.js +3 -1
- package/lib/types/components/LogoV2.d.ts +5 -1
- package/lib/types/components/LogoV2.d.ts.map +1 -1
- package/lib/types/components/LogoV2.js +12 -3
- package/lib/types/components/Markdown.d.ts.map +1 -1
- package/lib/types/components/Markdown.js +12 -3
- package/lib/types/components/MessageList.d.ts +43 -2
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +553 -41
- package/lib/types/components/ModelPicker.d.ts +4 -1
- package/lib/types/components/ModelPicker.d.ts.map +1 -1
- package/lib/types/components/ModelPicker.js +2 -2
- package/lib/types/components/PermissionsPicker.d.ts +21 -0
- package/lib/types/components/PermissionsPicker.d.ts.map +1 -0
- package/lib/types/components/PermissionsPicker.js +27 -0
- package/lib/types/components/PlanPicker.d.ts +15 -0
- package/lib/types/components/PlanPicker.d.ts.map +1 -0
- package/lib/types/components/PlanPicker.js +18 -0
- package/lib/types/components/PresetPicker.d.ts +4 -1
- package/lib/types/components/PresetPicker.d.ts.map +1 -1
- package/lib/types/components/PresetPicker.js +2 -2
- package/lib/types/components/PromptInput.d.ts +4 -3
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +273 -50
- package/lib/types/components/RewindPicker.d.ts +12 -1
- package/lib/types/components/RewindPicker.d.ts.map +1 -1
- package/lib/types/components/RewindPicker.js +6 -4
- package/lib/types/components/ScrollbarGutter.d.ts +28 -0
- package/lib/types/components/ScrollbarGutter.d.ts.map +1 -0
- package/lib/types/components/ScrollbarGutter.js +68 -0
- package/lib/types/components/Select.d.ts +8 -1
- package/lib/types/components/Select.d.ts.map +1 -1
- package/lib/types/components/Select.js +4 -2
- package/lib/types/components/SkillsPicker.d.ts +4 -1
- package/lib/types/components/SkillsPicker.d.ts.map +1 -1
- package/lib/types/components/SkillsPicker.js +2 -2
- package/lib/types/components/SubagentCard.d.ts +2 -1
- package/lib/types/components/SubagentCard.d.ts.map +1 -1
- package/lib/types/components/SubagentCard.js +6 -1
- package/lib/types/components/SubagentDashboard.d.ts +4 -0
- package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
- package/lib/types/components/SubagentDashboard.js +9 -1
- package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
- package/lib/types/components/SubagentDetailScene.js +5 -4
- package/lib/types/components/SuggestionCard.d.ts +63 -0
- package/lib/types/components/SuggestionCard.d.ts.map +1 -0
- package/lib/types/components/SuggestionCard.js +77 -0
- package/lib/types/components/ThemePicker.d.ts +4 -1
- package/lib/types/components/ThemePicker.d.ts.map +1 -1
- package/lib/types/components/ThemePicker.js +2 -2
- package/lib/types/components/ThinkingToggle.d.ts +4 -1
- package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
- package/lib/types/components/ThinkingToggle.js +2 -2
- package/lib/types/components/TimelineRail.d.ts +53 -0
- package/lib/types/components/TimelineRail.d.ts.map +1 -0
- package/lib/types/components/TimelineRail.js +190 -0
- package/lib/types/components/WorkspaceFlowPicker.d.ts +6 -1
- package/lib/types/components/WorkspaceFlowPicker.d.ts.map +1 -1
- package/lib/types/components/WorkspaceFlowPicker.js +4 -2
- package/lib/types/components/WorkspaceMenuPicker.d.ts +19 -0
- package/lib/types/components/WorkspaceMenuPicker.d.ts.map +1 -0
- package/lib/types/components/WorkspaceMenuPicker.js +19 -0
- package/lib/types/components/WorkspacePicker.d.ts +4 -1
- package/lib/types/components/WorkspacePicker.d.ts.map +1 -1
- package/lib/types/components/WorkspacePicker.js +2 -2
- package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
- package/lib/types/components/approvals/ApprovalPanel.js +5 -1
- package/lib/types/components/design-system/ListItem.d.ts +8 -1
- package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
- package/lib/types/components/design-system/ListItem.js +8 -3
- package/lib/types/components/messages/AssistantTextMessage.d.ts +5 -2
- package/lib/types/components/messages/AssistantTextMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantTextMessage.js +6 -2
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +10 -4
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +8 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +4 -2
- package/lib/types/components/messages/UserPromptMessage.d.ts +2 -1
- package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +34 -2
- package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
- package/lib/types/components/questions/PlanReviewPanel.js +13 -2
- package/lib/types/components/sessions/SessionListRow.d.ts +4 -1
- package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
- package/lib/types/components/sessions/SessionListRow.js +4 -2
- package/lib/types/components/trajectory/HotspotView.d.ts +7 -1
- package/lib/types/components/trajectory/HotspotView.d.ts.map +1 -1
- package/lib/types/components/trajectory/HotspotView.js +8 -2
- package/lib/types/components/trajectory/Ledger.d.ts +9 -1
- package/lib/types/components/trajectory/Ledger.d.ts.map +1 -1
- package/lib/types/components/trajectory/Ledger.js +23 -5
- package/lib/types/components/trajectory/WaveBand.d.ts +10 -1
- package/lib/types/components/trajectory/WaveBand.d.ts.map +1 -1
- package/lib/types/components/trajectory/WaveBand.js +7 -2
- package/lib/types/dsh-adapter/channel.d.ts +27 -6
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +432 -66
- package/lib/types/dsh-adapter/contract.d.ts +58 -18
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +124 -36
- package/lib/types/dsh-adapter/index.d.ts +10 -2
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +10 -13
- package/lib/types/dsh-adapter/plugin.d.ts +0 -9
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +136 -6
- package/lib/types/i18n.d.ts +194 -16
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +73 -10
- package/lib/types/ink/components/AlternateScreen.d.ts.map +1 -1
- package/lib/types/ink/components/AlternateScreen.js +13 -1
- package/lib/types/ink/components/App.d.ts +12 -1
- package/lib/types/ink/components/App.d.ts.map +1 -1
- package/lib/types/ink/components/App.js +101 -4
- package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
- package/lib/types/ink/components/ClockContext.js +2 -0
- package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
- package/lib/types/ink/components/ScrollBox.js +170 -91
- package/lib/types/ink/constants.d.ts +9 -0
- package/lib/types/ink/constants.d.ts.map +1 -1
- package/lib/types/ink/constants.js +9 -0
- package/lib/types/ink/events/click-event.d.ts +7 -16
- package/lib/types/ink/events/click-event.d.ts.map +1 -1
- package/lib/types/ink/events/click-event.js +16 -19
- package/lib/types/ink/events/event-handlers.d.ts +11 -1
- package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
- package/lib/types/ink/events/event-handlers.js +2 -0
- package/lib/types/ink/events/input-event.d.ts +9 -0
- package/lib/types/ink/events/input-event.d.ts.map +1 -1
- package/lib/types/ink/events/input-event.js +4 -0
- package/lib/types/ink/events/pointer-event.d.ts +61 -0
- package/lib/types/ink/events/pointer-event.d.ts.map +1 -0
- package/lib/types/ink/events/pointer-event.js +61 -0
- package/lib/types/ink/events/wheel-event.d.ts +17 -0
- package/lib/types/ink/events/wheel-event.d.ts.map +1 -0
- package/lib/types/ink/events/wheel-event.js +20 -0
- package/lib/types/ink/geometry-trace.d.ts +51 -0
- package/lib/types/ink/geometry-trace.d.ts.map +1 -0
- package/lib/types/ink/geometry-trace.js +83 -0
- package/lib/types/ink/hit-test.d.ts +56 -1
- package/lib/types/ink/hit-test.d.ts.map +1 -1
- package/lib/types/ink/hit-test.js +146 -8
- package/lib/types/ink/ink.d.ts +58 -1
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +268 -52
- package/lib/types/ink/node-cache.d.ts +5 -0
- package/lib/types/ink/node-cache.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.d.ts +8 -0
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +101 -12
- package/lib/types/ink/render-node-to-output.d.ts +16 -3
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +144 -25
- package/lib/types/ink/selection.d.ts +29 -4
- package/lib/types/ink/selection.d.ts.map +1 -1
- package/lib/types/ink/selection.js +41 -5
- package/lib/types/ink/timeline-rail.d.ts +138 -0
- package/lib/types/ink/timeline-rail.d.ts.map +1 -0
- package/lib/types/ink/timeline-rail.js +179 -0
- package/lib/types/ink/wrap-text.d.ts.map +1 -1
- package/lib/types/ink/wrap-text.js +47 -5
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +819 -337
- package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
- package/lib/types/screens/SessionBrowser.js +56 -29
- package/lib/types/screens/Settings.d.ts.map +1 -1
- package/lib/types/screens/Settings.js +64 -22
- package/lib/types/screens/StatusLine.d.ts +2 -1
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +17 -1
- package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
- package/lib/types/screens/TrajectoryScene.js +81 -14
- package/lib/types/screens/chatOverlay.d.ts +221 -0
- package/lib/types/screens/chatOverlay.d.ts.map +1 -0
- package/lib/types/screens/chatOverlay.js +114 -0
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +64 -10
- package/lib/types/tuiDisplayPrefs.d.ts +15 -0
- package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
- package/lib/types/tuiDisplayPrefs.js +9 -0
- package/lib/types/utils/debug.d.ts +10 -0
- package/lib/types/utils/debug.d.ts.map +1 -1
- package/lib/types/utils/debug.js +24 -0
- package/lib/types/utils/fileSuggestions.d.ts +19 -0
- package/lib/types/utils/fileSuggestions.d.ts.map +1 -0
- package/lib/types/utils/fileSuggestions.js +40 -0
- package/lib/types/utils/mentions.d.ts.map +1 -1
- package/lib/types/utils/mentions.js +3 -1
- package/lib/types/utils/paths.d.ts.map +1 -1
- package/lib/types/utils/paths.js +1 -0
- package/package.json +69 -66
|
@@ -12,19 +12,22 @@ import { clearResumeTarget, forgetSession, readResumeTarget, touchSession, write
|
|
|
12
12
|
import { appendSessionTitle, deleteSessionLog, ensureLegacySessionEventTypes, sessionsRoots } from './compat/index.js';
|
|
13
13
|
import { listSummaries, locateSession, noteBranch, previewSession, } from './sessions/index.js';
|
|
14
14
|
import { writeActivityFrames } from '../activityPrefs.js';
|
|
15
|
+
import { isPathLikeQuery, rankFileCandidates } from '../utils/fileSuggestions.js';
|
|
15
16
|
import { readEffortPref, writeEffortPref } from '../effortPrefs.js';
|
|
16
17
|
import { readModelPref, writeModelPref } from '../modelPrefs.js';
|
|
17
18
|
import { explicitModelRoute, recordedModelRoute, resolveModelRoute, validateModelRoute } from '../modelRoute.js';
|
|
18
19
|
import { readPresetPref, writePresetPref } from '../presetPrefs.js';
|
|
19
20
|
import { composePreset, resolvePersistedPreset, rosterOf, runningPresetOf, serviceForAgent } from './presets.js';
|
|
20
|
-
import { isPresetName } from '../components/activityFrames.js';
|
|
21
|
+
import { isPresetName, PRESET_NAMES } from '../components/activityFrames.js';
|
|
21
22
|
import { existsSync, statSync, writeFileSync } from 'node:fs';
|
|
22
23
|
import { logForDebugging } from '../utils/debug.js';
|
|
23
24
|
import { homeDir, LEGACY_DATA_DIR } from '../utils/paths.js';
|
|
24
25
|
import { extractMentions } from '../utils/mentions.js';
|
|
25
|
-
import { t } from '../i18n.js';
|
|
26
|
+
import { getLang, LANGS, t } from '../i18n.js';
|
|
27
|
+
import { AUTO_THEME_NAME, THEME_NAMES } from '../theme.js';
|
|
28
|
+
import { listCustomThemes } from '../customTheme.js';
|
|
26
29
|
import { modeDisplayName, resolveSessionModes } from '../sessionModes.js';
|
|
27
|
-
import { normalizeStatusBar, normalizeToolBackground } from '../tuiDisplayPrefs.js';
|
|
30
|
+
import { normalizeScrollGutter, normalizeStatusBar, normalizeToolBackground } from '../tuiDisplayPrefs.js';
|
|
28
31
|
import { SubagentActivityStore } from './subagents.js';
|
|
29
32
|
import { ActivityTracker } from 'dsh-working-activity/status';
|
|
30
33
|
import { featureOn } from 'dsh-working-activity/config';
|
|
@@ -42,7 +45,7 @@ import { installDecisionGuard } from './decision-guard.js';
|
|
|
42
45
|
import { commandOwner } from './command-attribution.js';
|
|
43
46
|
import { readGrantStore } from './grants.js';
|
|
44
47
|
import { hasCommandErrorCode, mapCommandError } from './command-errors.js';
|
|
45
|
-
import {
|
|
48
|
+
import { installedMeetsVersion } from './contract.js';
|
|
46
49
|
import { pluginsInfoLines } from './plugins-info.js';
|
|
47
50
|
import { cleanRenderText, cleanScalarText } from './sanitize.js';
|
|
48
51
|
/** `tui/input` return normalization: transform/handled/cancel or no opinion.
|
|
@@ -327,6 +330,72 @@ function toolResultText(event) {
|
|
|
327
330
|
return '';
|
|
328
331
|
return block.content.map(item => item.type === 'text' ? item.text : '').join('').trim();
|
|
329
332
|
}
|
|
333
|
+
/** Phase badge for the harness goal card — mirrors the panel's PhaseBadge. */
|
|
334
|
+
const GOAL_RESULT_BADGE = {
|
|
335
|
+
active: '● active',
|
|
336
|
+
paused: '⏸ paused',
|
|
337
|
+
blocked: '⛔ blocked',
|
|
338
|
+
complete: '✓ complete',
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* Summary cards for the harness's goal/todo tools. Their results are machine
|
|
342
|
+
* JSON (`{"goal":{…}}` / `{"todos":[…]}`) that would otherwise dump under the
|
|
343
|
+
* tool card as a raw `⎿ {"goal":…}` line. Matched by tool-name substring AND
|
|
344
|
+
* payload shape, so unrelated tools and plain-text results pass through to
|
|
345
|
+
* the registry/raw-text path untouched. Runs on the live stream and replay.
|
|
346
|
+
*/
|
|
347
|
+
function harnessToolResultView(name, data) {
|
|
348
|
+
const lower = name.toLowerCase();
|
|
349
|
+
const isGoalTool = lower.includes('goal');
|
|
350
|
+
const isTodoTool = lower.includes('todo');
|
|
351
|
+
if (!isGoalTool && !isTodoTool)
|
|
352
|
+
return undefined;
|
|
353
|
+
const block = data.message.content[0];
|
|
354
|
+
if (block === undefined || block.type !== 'tool-result')
|
|
355
|
+
return undefined;
|
|
356
|
+
const text = block.content.map(item => item.type === 'text' ? item.text : '').join('').trim();
|
|
357
|
+
if (text === '' || !text.startsWith('{'))
|
|
358
|
+
return undefined;
|
|
359
|
+
let parsed;
|
|
360
|
+
try {
|
|
361
|
+
parsed = JSON.parse(text);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
367
|
+
return undefined;
|
|
368
|
+
const record = parsed;
|
|
369
|
+
if (isGoalTool && typeof record.goal === 'object' && record.goal !== null) {
|
|
370
|
+
const goal = record.goal;
|
|
371
|
+
if (typeof goal.objective === 'string' && typeof goal.phase === 'string') {
|
|
372
|
+
const badge = GOAL_RESULT_BADGE[goal.phase] ?? goal.phase;
|
|
373
|
+
const rounds = typeof goal.roundsStarted === 'number' && typeof goal.maxGoalRounds === 'number'
|
|
374
|
+
? ` · ${goal.roundsStarted}/${goal.maxGoalRounds}`
|
|
375
|
+
: '';
|
|
376
|
+
const activation = typeof record.activation === 'string' ? ` · ${record.activation}` : '';
|
|
377
|
+
const lines = [`🎯 ${goal.objective}`, `${badge}${rounds}${activation}`];
|
|
378
|
+
const blocked = goal.blockedReason?.message;
|
|
379
|
+
if (typeof blocked === 'string')
|
|
380
|
+
lines.push(`⛔ ${blocked}`);
|
|
381
|
+
return { card: 'generic', content: lines.map(line => ({ type: 'text', text: line })) };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (isTodoTool && Array.isArray(record.todos)) {
|
|
385
|
+
const rows = record.todos;
|
|
386
|
+
const done = rows.filter(row => row.status === 'completed').length;
|
|
387
|
+
const lines = [`todos ✓ ${done}/${rows.length}`];
|
|
388
|
+
for (const row of rows) {
|
|
389
|
+
if (row.status !== 'in_progress' || typeof row.content !== 'string')
|
|
390
|
+
continue;
|
|
391
|
+
lines.push(`● ${row.content}`);
|
|
392
|
+
if (lines.length >= 4)
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
return { card: 'generic', content: lines.map(line => ({ type: 'text', text: line })) };
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
330
399
|
function toolErrorText(event) {
|
|
331
400
|
const failure = event.data.error;
|
|
332
401
|
if (failure === undefined)
|
|
@@ -432,8 +501,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
432
501
|
try {
|
|
433
502
|
runtime.interrupt(target, { kind: 'ancestor', agent });
|
|
434
503
|
subagentStore.onCancelled(agentId, 'interrupted');
|
|
435
|
-
|
|
436
|
-
syncSubagentRows();
|
|
504
|
+
syncSubagentsNow();
|
|
437
505
|
state.emit();
|
|
438
506
|
return true;
|
|
439
507
|
}
|
|
@@ -466,9 +534,12 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
466
534
|
/**
|
|
467
535
|
* Sync subagentStore state into ChatRows (insert/update in state.rows).
|
|
468
536
|
* Called whenever subagent state changes (spawned/completed/failed/output).
|
|
537
|
+
* Accepts a caller-taken snapshot to avoid the double copy on the hot path
|
|
538
|
+
* (session/event fires per subagent token: snapshot here + snapshot in the
|
|
539
|
+
* caller = two full state copies before emitStream's 16ms throttle).
|
|
469
540
|
*/
|
|
470
|
-
const syncSubagentRows = () => {
|
|
471
|
-
const snapshot = subagentStore.snapshot();
|
|
541
|
+
const syncSubagentRows = (preSnapshot) => {
|
|
542
|
+
const snapshot = preSnapshot ?? subagentStore.snapshot();
|
|
472
543
|
for (const sub of snapshot) {
|
|
473
544
|
let row = subagentRowsByAgentId.get(sub.agentId);
|
|
474
545
|
if (!row) {
|
|
@@ -545,6 +616,32 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
545
616
|
const listeners = new Set();
|
|
546
617
|
/** True while a frame-aligned stream notification is pending (emitStream). */
|
|
547
618
|
let streamNotifyScheduled = false;
|
|
619
|
+
/** True while subagent assistant/chunk deltas have deferred their
|
|
620
|
+
* snapshot+projection to the frame-aligned flush (emitStream's timer).
|
|
621
|
+
* Chunks arrive at token rate (100-300 events/s) and the projection is a
|
|
622
|
+
* full deep state copy (SubagentActivityStore.snapshot) plus a SubagentRow
|
|
623
|
+
* rebuild per tracked agent — running that per token sits BEFORE
|
|
624
|
+
* emitStream's 16ms coalescing and defeats it. Non-chunk events
|
|
625
|
+
* (tool/call, subagent/end, interrupt) project immediately and clear
|
|
626
|
+
* this flag, so lifecycle transitions stay synchronous. */
|
|
627
|
+
let subagentStreamDirty = false;
|
|
628
|
+
/** Deferred projection for the frame-aligned flush: runs INSIDE the
|
|
629
|
+
* emitStream timer, before listeners wake, so React always reads fully
|
|
630
|
+
* projected rows. No-op unless a chunk marked the projection dirty. */
|
|
631
|
+
const flushSubagentStream = () => {
|
|
632
|
+
if (!subagentStreamDirty)
|
|
633
|
+
return;
|
|
634
|
+
subagentStreamDirty = false;
|
|
635
|
+
state.subagents = subagentStore.snapshot();
|
|
636
|
+
syncSubagentRows(state.subagents);
|
|
637
|
+
};
|
|
638
|
+
/** Immediate projection; supersedes any pending deferred flush (the fresh
|
|
639
|
+
* snapshot already contains everything the deferred pass would project). */
|
|
640
|
+
const syncSubagentsNow = () => {
|
|
641
|
+
subagentStreamDirty = false;
|
|
642
|
+
state.subagents = subagentStore.snapshot();
|
|
643
|
+
syncSubagentRows(state.subagents);
|
|
644
|
+
};
|
|
548
645
|
// foldRows incremental cursor (see foldRows): rows only append past the
|
|
549
646
|
// fold line, so each pass touches only newly-eligible rows.
|
|
550
647
|
const foldCursor = { rows: null, index: 0 };
|
|
@@ -910,13 +1007,14 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
910
1007
|
applyEffort(found);
|
|
911
1008
|
return true;
|
|
912
1009
|
};
|
|
913
|
-
/** Whether the installed command service takes composer images:
|
|
914
|
-
* gate
|
|
915
|
-
*
|
|
1010
|
+
/** Whether the installed command service takes composer images: version
|
|
1011
|
+
* gate (composer images arrived on 0.1.0-rc.8 and every later family —
|
|
1012
|
+
* 0.1.1 included — keeps the 4-param shape) with a structural fallback,
|
|
1013
|
+
* so a failed manifest probe (bundlers, exotic loaders) still lands on
|
|
1014
|
+
* the 4-param rc.8 shape at runtime. */
|
|
916
1015
|
const commandServiceSupportsImages = (service) => {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
return line >= 8;
|
|
1016
|
+
if (installedMeetsVersion('@deepseek-ai/dsh-commands', '0.1.0-rc.8'))
|
|
1017
|
+
return true;
|
|
920
1018
|
return typeof service.execute?.length === 'number'
|
|
921
1019
|
&& service.execute.length >= 4;
|
|
922
1020
|
};
|
|
@@ -1129,6 +1227,74 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1129
1227
|
const index = deriveModeIndex(agent.session.events);
|
|
1130
1228
|
await applyMode(sessionModes[(index + 1) % sessionModes.length]);
|
|
1131
1229
|
};
|
|
1230
|
+
// Session-lifetime candidate pool for non-path queries. The load promise is
|
|
1231
|
+
// shared so concurrent first keystrokes cannot kick off duplicate scans, and
|
|
1232
|
+
// it is keyed by cwd so a /workspace switch or resumed session never reuses
|
|
1233
|
+
// another directory's listing.
|
|
1234
|
+
const fileCandidateCache = { cwd: '', load: undefined };
|
|
1235
|
+
// `/model <provider/id>` completion: the model catalog is async (one llm
|
|
1236
|
+
// listModels per provider), so the first keystrokes that could be heading
|
|
1237
|
+
// for /model warm a session-lifetime cache — the shared promise dedupes
|
|
1238
|
+
// concurrent triggers, children() synchronously serves whatever has landed,
|
|
1239
|
+
// and the arrival state.emit() reopens the menu mid-typing. switchModel's
|
|
1240
|
+
// success path drops the cache so the [current] tag re-resolves against
|
|
1241
|
+
// the new route.
|
|
1242
|
+
const modelNodeCache = {
|
|
1243
|
+
nodes: undefined,
|
|
1244
|
+
load: undefined,
|
|
1245
|
+
};
|
|
1246
|
+
const warmModelNodes = () => {
|
|
1247
|
+
if (modelNodeCache.load !== undefined)
|
|
1248
|
+
return;
|
|
1249
|
+
modelNodeCache.load = state.listModels().then((list) => {
|
|
1250
|
+
modelNodeCache.nodes = list.map((model) => ({
|
|
1251
|
+
name: `${model.provider}/${model.id}`,
|
|
1252
|
+
description: model.name,
|
|
1253
|
+
...(state.provider === model.provider && state.model === model.id
|
|
1254
|
+
? { tag: 'current' }
|
|
1255
|
+
: {}),
|
|
1256
|
+
}));
|
|
1257
|
+
state.emit();
|
|
1258
|
+
}).catch(() => {
|
|
1259
|
+
// listModels already swallows per-provider failures; this only fires
|
|
1260
|
+
// when the llm service shape itself is missing — settle on an empty
|
|
1261
|
+
// menu rather than retrying on every keystroke.
|
|
1262
|
+
modelNodeCache.nodes = [];
|
|
1263
|
+
});
|
|
1264
|
+
};
|
|
1265
|
+
// `/preset <id>` completion: same warm-cache pattern as models. The
|
|
1266
|
+
// current/default tags resolve at children() time (sync state reads), so
|
|
1267
|
+
// no cache invalidation is needed on switch.
|
|
1268
|
+
const presetOptionCache = {
|
|
1269
|
+
list: undefined,
|
|
1270
|
+
load: undefined,
|
|
1271
|
+
};
|
|
1272
|
+
const warmPresetOptions = () => {
|
|
1273
|
+
if (presetOptionCache.load !== undefined)
|
|
1274
|
+
return;
|
|
1275
|
+
presetOptionCache.load = state.listPresets().then((list) => {
|
|
1276
|
+
presetOptionCache.list = list;
|
|
1277
|
+
state.emit();
|
|
1278
|
+
}).catch(() => {
|
|
1279
|
+
presetOptionCache.list = [];
|
|
1280
|
+
});
|
|
1281
|
+
};
|
|
1282
|
+
// `/effort <id>` completion: state.effortLevels is the sync vocabulary
|
|
1283
|
+
// (populated on route changes); when still unknown, one best-effort
|
|
1284
|
+
// resolveEfforts warms it. `tried` caps the retry — resolveEfforts
|
|
1285
|
+
// notifies on hard errors, so keystroke-time retries would spam.
|
|
1286
|
+
const effortWarm = { tried: false };
|
|
1287
|
+
const warmEffortLevels = () => {
|
|
1288
|
+
if (state.effortLevels !== undefined || effortWarm.tried)
|
|
1289
|
+
return;
|
|
1290
|
+
effortWarm.tried = true;
|
|
1291
|
+
void resolveEfforts().then((resolved) => {
|
|
1292
|
+
if (resolved === 'unavailable' || resolved === 'error')
|
|
1293
|
+
return;
|
|
1294
|
+
effortWarm.tried = false;
|
|
1295
|
+
state.emit();
|
|
1296
|
+
}).catch(() => { });
|
|
1297
|
+
};
|
|
1132
1298
|
const state = {
|
|
1133
1299
|
effortLevels: undefined,
|
|
1134
1300
|
version: 0,
|
|
@@ -1162,6 +1328,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1162
1328
|
diffLayout: options.diffLayout ?? 'auto',
|
|
1163
1329
|
thinkingFold: options.thinkingFold ?? 'preview',
|
|
1164
1330
|
toolBackground: normalizeToolBackground(options.toolBackground),
|
|
1331
|
+
scrollGutter: normalizeScrollGutter(options.scrollGutter),
|
|
1165
1332
|
statusBar: normalizeStatusBar(options.statusBar),
|
|
1166
1333
|
whale: options.whale !== false,
|
|
1167
1334
|
minimal: options.minimal === true,
|
|
@@ -1174,7 +1341,94 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1174
1341
|
pending: [],
|
|
1175
1342
|
commandList: LOCAL_COMMANDS,
|
|
1176
1343
|
commandCompletions(input) {
|
|
1344
|
+
// Warm the async vocabularies as soon as the input could be heading
|
|
1345
|
+
// for their commands (`/m`, `/pre`, …) — by the time a trailing space
|
|
1346
|
+
// asks children() for nodes, the fetch has usually landed.
|
|
1347
|
+
const head = input.slice(1).split(/[\t ]/)[0]?.toLowerCase() ?? '';
|
|
1348
|
+
if (head !== '') {
|
|
1349
|
+
if ('model'.startsWith(head))
|
|
1350
|
+
warmModelNodes();
|
|
1351
|
+
if ('preset'.startsWith(head))
|
|
1352
|
+
warmPresetOptions();
|
|
1353
|
+
if ('effort'.startsWith(head))
|
|
1354
|
+
warmEffortLevels();
|
|
1355
|
+
}
|
|
1177
1356
|
return completeCommands(input, state.commandList, (path) => {
|
|
1357
|
+
if (path.length === 1 && path[0] === 'model') {
|
|
1358
|
+
// provider/id specs, current model tagged; see modelNodeCache.
|
|
1359
|
+
warmModelNodes();
|
|
1360
|
+
return modelNodeCache.nodes ?? [];
|
|
1361
|
+
}
|
|
1362
|
+
if (path.length === 1 && path[0] === 'lang') {
|
|
1363
|
+
return [
|
|
1364
|
+
{ name: 'status', description: 'Show the current UI language', descriptionKey: 'sugg-status-desc' },
|
|
1365
|
+
...LANGS.map((lang) => ({
|
|
1366
|
+
name: lang,
|
|
1367
|
+
description: `Switch the UI language to ${lang}`,
|
|
1368
|
+
descriptionKey: lang === 'zh' ? 'sugg-lang-zh-desc' : 'sugg-lang-en-desc',
|
|
1369
|
+
...(getLang() === lang ? { tag: 'current' } : {}),
|
|
1370
|
+
})),
|
|
1371
|
+
];
|
|
1372
|
+
}
|
|
1373
|
+
if (path.length === 1 && path[0] === 'theme') {
|
|
1374
|
+
return [
|
|
1375
|
+
{ name: 'status', description: 'Show the current theme', descriptionKey: 'sugg-status-desc' },
|
|
1376
|
+
{ name: AUTO_THEME_NAME, description: 'Follow the terminal background', descriptionKey: 'sugg-theme-auto-desc' },
|
|
1377
|
+
...THEME_NAMES.map((name) => ({
|
|
1378
|
+
name,
|
|
1379
|
+
description: `Built-in theme ${name}`,
|
|
1380
|
+
descriptionKey: 'sugg-theme-builtin-desc',
|
|
1381
|
+
})),
|
|
1382
|
+
...listCustomThemes()
|
|
1383
|
+
.filter((spec) => spec.name !== AUTO_THEME_NAME)
|
|
1384
|
+
.map((spec) => ({
|
|
1385
|
+
name: spec.name,
|
|
1386
|
+
description: `User theme (${spec.base} base)`,
|
|
1387
|
+
descriptionKey: 'sugg-theme-user-desc',
|
|
1388
|
+
})),
|
|
1389
|
+
];
|
|
1390
|
+
}
|
|
1391
|
+
if (path.length === 1 && path[0] === 'effort') {
|
|
1392
|
+
warmEffortLevels();
|
|
1393
|
+
return [
|
|
1394
|
+
{ name: 'status', description: 'Show the current reasoning effort', descriptionKey: 'sugg-status-desc' },
|
|
1395
|
+
...(state.effortLevels ?? []).map((id) => ({
|
|
1396
|
+
name: id,
|
|
1397
|
+
description: 'Reasoning effort level',
|
|
1398
|
+
descriptionKey: 'sugg-effort-level-desc',
|
|
1399
|
+
...(state.reasoningEffort === id ? { tag: 'current' } : {}),
|
|
1400
|
+
})),
|
|
1401
|
+
];
|
|
1402
|
+
}
|
|
1403
|
+
if (path.length === 1 && path[0] === 'preset') {
|
|
1404
|
+
warmPresetOptions();
|
|
1405
|
+
return [
|
|
1406
|
+
{ name: 'status', description: 'Show the current agent preset', descriptionKey: 'sugg-status-desc' },
|
|
1407
|
+
...(presetOptionCache.list ?? []).map((preset) => ({
|
|
1408
|
+
name: preset.id,
|
|
1409
|
+
description: preset.description ?? preset.name ?? preset.id,
|
|
1410
|
+
...(preset.id === state.agentPreset
|
|
1411
|
+
? { tag: 'current' }
|
|
1412
|
+
: preset.isDefault
|
|
1413
|
+
? { tag: 'default' }
|
|
1414
|
+
: {}),
|
|
1415
|
+
})),
|
|
1416
|
+
];
|
|
1417
|
+
}
|
|
1418
|
+
if (path.length === 1 && path[0] === 'activity') {
|
|
1419
|
+
return [
|
|
1420
|
+
{ name: 'status', description: 'Show the current activity preset', descriptionKey: 'sugg-status-desc' },
|
|
1421
|
+
{ name: 'frames', description: 'List or switch frame presets', descriptionKey: 'sugg-activity-frames-desc' },
|
|
1422
|
+
];
|
|
1423
|
+
}
|
|
1424
|
+
if (path.length === 2 && path[0] === 'activity' && path[1] === 'frames') {
|
|
1425
|
+
return PRESET_NAMES.map((name) => ({
|
|
1426
|
+
name,
|
|
1427
|
+
description: 'Animation frame preset',
|
|
1428
|
+
descriptionKey: 'sugg-activity-frame-desc',
|
|
1429
|
+
...(state.activityFrames === name ? { tag: 'current' } : {}),
|
|
1430
|
+
}));
|
|
1431
|
+
}
|
|
1178
1432
|
if (path.length === 1 && path[0] === 'workspace') {
|
|
1179
1433
|
const builtins = [
|
|
1180
1434
|
{ name: 'resume', description: 'Switch to another workspace', descriptionKey: 'cmd-desc-workspace-resume' },
|
|
@@ -1193,6 +1447,21 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1193
1447
|
})),
|
|
1194
1448
|
];
|
|
1195
1449
|
}
|
|
1450
|
+
if (path.length === 1 && path[0] === 'permission') {
|
|
1451
|
+
// Sandbox-preset vocabulary of the `/permission` picker — Tab
|
|
1452
|
+
// completes it for keyboard users, Enter still dispatches.
|
|
1453
|
+
return [
|
|
1454
|
+
{ name: 'read-only', description: 'Read-only session: no file writes, no commands', descriptionKey: 'permission-preset-readonly-desc' },
|
|
1455
|
+
{ name: 'workspace-write', description: 'Read/write inside the workspace; writes need a prior read', descriptionKey: 'permission-preset-workspace-write-desc' },
|
|
1456
|
+
{ name: 'danger-full-access', description: 'Unrestricted access, no approvals', descriptionKey: 'permission-preset-full-access-desc' },
|
|
1457
|
+
];
|
|
1458
|
+
}
|
|
1459
|
+
if (path.length === 1 && path[0] === 'plan') {
|
|
1460
|
+
return [
|
|
1461
|
+
{ name: 'on', description: 'Enter plan mode: read-only, plan before acting', descriptionKey: 'plan-mode-on-desc' },
|
|
1462
|
+
{ name: 'off', description: 'Exit plan mode, back to normal execution', descriptionKey: 'plan-mode-off-desc' },
|
|
1463
|
+
];
|
|
1464
|
+
}
|
|
1196
1465
|
return commandTrees?.children(path) ?? [];
|
|
1197
1466
|
});
|
|
1198
1467
|
},
|
|
@@ -1233,6 +1502,9 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1233
1502
|
streamNotifyScheduled = true;
|
|
1234
1503
|
const timer = setTimeout(() => {
|
|
1235
1504
|
streamNotifyScheduled = false;
|
|
1505
|
+
// Deferred subagent projection rides this same frame: flush BEFORE
|
|
1506
|
+
// the listeners wake so React reads fully projected rows.
|
|
1507
|
+
flushSubagentStream();
|
|
1236
1508
|
foldRows(state.rows, MAX_ROWS, foldCursor);
|
|
1237
1509
|
for (const listener of listeners)
|
|
1238
1510
|
listener();
|
|
@@ -2043,6 +2315,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2043
2315
|
state.agentPreset = modelComposed.agentPreset;
|
|
2044
2316
|
state.model = model;
|
|
2045
2317
|
state.provider = provider;
|
|
2318
|
+
// /model completion cache: the [current] tag was resolved at fetch
|
|
2319
|
+
// time — drop the cache so the next `/model ` refetches for the new
|
|
2320
|
+
// route.
|
|
2321
|
+
modelNodeCache.nodes = undefined;
|
|
2322
|
+
modelNodeCache.load = undefined;
|
|
2046
2323
|
state.tps = undefined;
|
|
2047
2324
|
state.tpsSamples = [];
|
|
2048
2325
|
state.lastUsage = undefined;
|
|
@@ -2153,6 +2430,13 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2153
2430
|
state.toolBackground = normalized;
|
|
2154
2431
|
state.emit();
|
|
2155
2432
|
},
|
|
2433
|
+
setScrollGutter(mode) {
|
|
2434
|
+
const normalized = normalizeScrollGutter(mode);
|
|
2435
|
+
if (normalized === state.scrollGutter)
|
|
2436
|
+
return;
|
|
2437
|
+
state.scrollGutter = normalized;
|
|
2438
|
+
state.emit();
|
|
2439
|
+
},
|
|
2156
2440
|
setStatusBar(config) {
|
|
2157
2441
|
const next = normalizeStatusBar({ ...state.statusBar, ...config });
|
|
2158
2442
|
const changed = Object.keys(next).some(key => next[key] !== state.statusBar[key]);
|
|
@@ -2460,9 +2744,33 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2460
2744
|
signal: options?.signal,
|
|
2461
2745
|
});
|
|
2462
2746
|
},
|
|
2463
|
-
|
|
2747
|
+
async listFileCandidates(query, options) {
|
|
2464
2748
|
const fs = ctx.get('fs');
|
|
2465
|
-
|
|
2749
|
+
if (!fs || options?.signal?.aborted)
|
|
2750
|
+
return [];
|
|
2751
|
+
if (isPathLikeQuery(query)) {
|
|
2752
|
+
return listPathCandidates(fs, state.cwd, query, options?.signal, options?.topK ?? 50);
|
|
2753
|
+
}
|
|
2754
|
+
if (fileCandidateCache.cwd !== state.cwd) {
|
|
2755
|
+
fileCandidateCache.cwd = state.cwd;
|
|
2756
|
+
fileCandidateCache.load = undefined;
|
|
2757
|
+
}
|
|
2758
|
+
fileCandidateCache.load ??= listFilesDeepCandidates(fs, state.cwd).then(candidates => {
|
|
2759
|
+
if (candidates.length > 0)
|
|
2760
|
+
return candidates;
|
|
2761
|
+
// An empty scan is not worth caching forever — retry on next query.
|
|
2762
|
+
fileCandidateCache.load = undefined;
|
|
2763
|
+
return candidates;
|
|
2764
|
+
});
|
|
2765
|
+
const candidates = await fileCandidateCache.load;
|
|
2766
|
+
if (options?.signal?.aborted)
|
|
2767
|
+
return [];
|
|
2768
|
+
return rankFileCandidates(candidates, query, options?.topK ?? 50);
|
|
2769
|
+
},
|
|
2770
|
+
async listFiles() {
|
|
2771
|
+
const fs = ctx.get('fs');
|
|
2772
|
+
const candidates = await listFilesDeepCandidates(fs, state.cwd);
|
|
2773
|
+
return candidates.map(candidate => candidate.path);
|
|
2466
2774
|
},
|
|
2467
2775
|
async listSessions() {
|
|
2468
2776
|
// Every stored session, classified and unfiltered. Which of them a
|
|
@@ -3310,6 +3618,12 @@ ${output}
|
|
|
3310
3618
|
* its result-time contextual diff back from here). */
|
|
3311
3619
|
const presentResultView = (name, rawArgs, data) => {
|
|
3312
3620
|
try {
|
|
3621
|
+
// Harness goal/todo tools first: their raw JSON reads as noise in the
|
|
3622
|
+
// transcript — fold recognizable shapes into a summary card before the
|
|
3623
|
+
// registry gets a chance to (not) know them.
|
|
3624
|
+
const local = harnessToolResultView(name, data);
|
|
3625
|
+
if (local !== undefined)
|
|
3626
|
+
return local;
|
|
3313
3627
|
const tool = toolsRegistry?.get(name, agent);
|
|
3314
3628
|
if (tool?.presentResult === undefined)
|
|
3315
3629
|
return undefined;
|
|
@@ -3442,9 +3756,21 @@ ${output}
|
|
|
3442
3756
|
state.spinnerMode = 'requesting';
|
|
3443
3757
|
}
|
|
3444
3758
|
};
|
|
3759
|
+
/** Fold one goal mutation into the channel's goal projection. */
|
|
3760
|
+
const applyGoalChange = (change) => {
|
|
3761
|
+
if (change.operation === 'clear') {
|
|
3762
|
+
state.goal = undefined;
|
|
3763
|
+
}
|
|
3764
|
+
else if (change.goal !== undefined) {
|
|
3765
|
+
state.goal = {
|
|
3766
|
+
...change.goal,
|
|
3767
|
+
roundsStarted: change.roundsStarted ?? state.goal?.roundsStarted ?? 0,
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
};
|
|
3445
3771
|
/**
|
|
3446
3772
|
* Fold one goal-sourced message into the channel's goal projection.
|
|
3447
|
-
* Round-zero goal messages carry the full durable snapshot (or a clear
|
|
3773
|
+
* Round-zero goal messages may carry the full durable snapshot (or a clear
|
|
3448
3774
|
* tombstone) in their source; positive-round messages are admitted
|
|
3449
3775
|
* continuation prompts that only advance the rounds counter.
|
|
3450
3776
|
*/
|
|
@@ -3464,15 +3790,7 @@ ${output}
|
|
|
3464
3790
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- durable replay data may not match the static type
|
|
3465
3791
|
if (change === undefined || change.kind !== 'goal/change')
|
|
3466
3792
|
return;
|
|
3467
|
-
|
|
3468
|
-
state.goal = undefined;
|
|
3469
|
-
}
|
|
3470
|
-
else if (change.goal !== undefined) {
|
|
3471
|
-
state.goal = {
|
|
3472
|
-
...change.goal,
|
|
3473
|
-
roundsStarted: change.roundsStarted ?? state.goal?.roundsStarted ?? 0,
|
|
3474
|
-
};
|
|
3475
|
-
}
|
|
3793
|
+
applyGoalChange(change);
|
|
3476
3794
|
};
|
|
3477
3795
|
/** True while the durable transcript is being replayed (boot /resume /
|
|
3478
3796
|
* rewind / model-switch fork). The assistant/message reasoning-rebuild
|
|
@@ -3500,6 +3818,15 @@ ${output}
|
|
|
3500
3818
|
}
|
|
3501
3819
|
};
|
|
3502
3820
|
const renderEvent = (event) => {
|
|
3821
|
+
// Top-level `goal/change` events are how the goal service actually
|
|
3822
|
+
// records durable goal mutations (create/edit/pause/resume/complete/
|
|
3823
|
+
// block/clear) — confirmed in production logs. The pinned peer's
|
|
3824
|
+
// SessionEvent union predates the type, so admit it structurally: the
|
|
3825
|
+
// goal chip and panel stay dark without this fold.
|
|
3826
|
+
if (event.type === 'goal/change') {
|
|
3827
|
+
applyGoalChange(event.data);
|
|
3828
|
+
return;
|
|
3829
|
+
}
|
|
3503
3830
|
switch (event.type) {
|
|
3504
3831
|
case 'user/message': {
|
|
3505
3832
|
// Compaction checkpoint: `source = { kind: 'plugin', plugin:
|
|
@@ -3542,10 +3869,13 @@ ${output}
|
|
|
3542
3869
|
contextWarned = false;
|
|
3543
3870
|
break;
|
|
3544
3871
|
}
|
|
3545
|
-
// Same-session goal domain:
|
|
3546
|
-
//
|
|
3547
|
-
//
|
|
3548
|
-
//
|
|
3872
|
+
// Same-session goal domain: goal-sourced messages are the round
|
|
3873
|
+
// driver's continuation prompts (positive rounds advance the
|
|
3874
|
+
// counter); some hosts also inline the durable snapshot in a
|
|
3875
|
+
// round-zero source. They are not transcript bubbles — they drive
|
|
3876
|
+
// the goal panel's live projection (replayed on resume/rewind like
|
|
3877
|
+
// every other event; the snapshot itself arrives as the top-level
|
|
3878
|
+
// `goal/change` event admitted above).
|
|
3549
3879
|
if (event.data.source.kind === 'goal') {
|
|
3550
3880
|
applyGoalEvent(event);
|
|
3551
3881
|
break;
|
|
@@ -4127,12 +4457,18 @@ ${output}
|
|
|
4127
4457
|
const subagentId = subagentStore.getSubagentIdBySession(session);
|
|
4128
4458
|
if (subagentId) {
|
|
4129
4459
|
subagentStore.onSessionEvent(subagentId, event);
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4460
|
+
if (event.type === 'assistant/chunk') {
|
|
4461
|
+
// Token-rate path (100-300 events/s): the store append stays
|
|
4462
|
+
// synchronous (cheap); the expensive snapshot + row projection
|
|
4463
|
+
// defers to the frame-aligned flush inside emitStream's 16ms
|
|
4464
|
+
// timer, so it coalesces exactly like the main-agent stream.
|
|
4465
|
+
subagentStreamDirty = true;
|
|
4133
4466
|
state.emitStream();
|
|
4134
|
-
|
|
4467
|
+
}
|
|
4468
|
+
else {
|
|
4469
|
+
syncSubagentsNow();
|
|
4135
4470
|
state.emit();
|
|
4471
|
+
}
|
|
4136
4472
|
return;
|
|
4137
4473
|
}
|
|
4138
4474
|
// Otherwise handle main agent session
|
|
@@ -4195,8 +4531,7 @@ ${output}
|
|
|
4195
4531
|
catch {
|
|
4196
4532
|
// Session discovery is best-effort and must not break the parent turn.
|
|
4197
4533
|
}
|
|
4198
|
-
|
|
4199
|
-
syncSubagentRows();
|
|
4534
|
+
syncSubagentsNow();
|
|
4200
4535
|
state.emit();
|
|
4201
4536
|
});
|
|
4202
4537
|
const disposeEnd = ctx.on('subagent/end', (info) => {
|
|
@@ -4218,8 +4553,7 @@ ${output}
|
|
|
4218
4553
|
subagentStore.onCancelled(info.id, info.stopReason, output);
|
|
4219
4554
|
else
|
|
4220
4555
|
subagentStore.onFailed(info.id, info.stopReason || 'Unknown error');
|
|
4221
|
-
|
|
4222
|
-
syncSubagentRows();
|
|
4556
|
+
syncSubagentsNow();
|
|
4223
4557
|
state.emit();
|
|
4224
4558
|
});
|
|
4225
4559
|
return () => {
|
|
@@ -4373,33 +4707,64 @@ function usageOutputTokens(usage) {
|
|
|
4373
4707
|
? value
|
|
4374
4708
|
: undefined;
|
|
4375
4709
|
}
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4710
|
+
async function listPathCandidates(fs, cwd, query, signal, topK) {
|
|
4711
|
+
const normalized = query.replaceAll('\\', '/');
|
|
4712
|
+
const slash = normalized.lastIndexOf('/');
|
|
4713
|
+
// `.` / `..` without a trailing separator are whole-directory queries too.
|
|
4714
|
+
const bareDir = slash < 0 && (normalized === '.' || normalized === '..' || normalized === '~');
|
|
4715
|
+
const directoryPart = slash < 0 ? (bareDir ? `${normalized}/` : '') : normalized.slice(0, slash + 1);
|
|
4716
|
+
const nameQuery = slash < 0 || bareDir ? '' : normalized.slice(slash + 1);
|
|
4717
|
+
// `~/` expands against the host home (matches the cwd resolution rules);
|
|
4718
|
+
// drive-letter and POSIX-absolute prefixes pass through untouched.
|
|
4719
|
+
const expanded = directoryPart === '~/'
|
|
4720
|
+
? `${homeDir()}/`
|
|
4721
|
+
: directoryPart.startsWith('/') || /^[A-Za-z]:\//.test(directoryPart)
|
|
4722
|
+
? directoryPart
|
|
4723
|
+
: join(cwd, directoryPart || '.');
|
|
4724
|
+
try {
|
|
4725
|
+
if (signal?.aborted)
|
|
4726
|
+
return [];
|
|
4727
|
+
const target = await fs.resolve(expanded);
|
|
4728
|
+
const entries = (await fs.listDir(target)).slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
4729
|
+
return rankFileCandidates(entries.filter(entry => entry.type === 'file' || entry.type === 'directory').map(entry => {
|
|
4730
|
+
const path = `${directoryPart}${entry.name}${entry.type === 'directory' ? '/' : ''}`;
|
|
4731
|
+
return { id: path, path, displayPath: path, name: entry.name, kind: entry.type, score: 0 };
|
|
4732
|
+
}), nameQuery, topK);
|
|
4733
|
+
}
|
|
4734
|
+
catch {
|
|
4735
|
+
return [];
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
async function listFilesDeepCandidates(fs, root, signal) {
|
|
4385
4739
|
if (!fs)
|
|
4386
4740
|
return [];
|
|
4387
4741
|
const out = [];
|
|
4388
4742
|
const SKIP = new Set(['node_modules', '.git', '.hg', '.svn', '.DS_Store', 'dist']);
|
|
4389
4743
|
const BUILD_DIR = /^(?:build(?:[-_].*)?|cmake-build(?:[-_].*)?)$/i;
|
|
4390
|
-
const
|
|
4391
|
-
const
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4744
|
+
const queue = [{ dir: root, prefix: '', index: 0 }];
|
|
4745
|
+
const visited = new Set();
|
|
4746
|
+
const maxFiles = 100;
|
|
4747
|
+
const maxDirectories = 100;
|
|
4748
|
+
let fileCount = 0;
|
|
4749
|
+
let dirCount = 0;
|
|
4750
|
+
// Round-robin: each directory yields ONE non-skipped entry per visit before
|
|
4751
|
+
// it re-queues, so a large early sibling (e.g. `generated/` with 120 files)
|
|
4752
|
+
// cannot starve `src/` out of the per-kind budgets. This is the regression
|
|
4753
|
+
// contract pinned by scripts/verify-file-completion.mjs.
|
|
4754
|
+
while (queue.length && fileCount < maxFiles && dirCount < maxDirectories) {
|
|
4755
|
+
if (signal?.aborted)
|
|
4756
|
+
return [];
|
|
4757
|
+
const current = queue.shift();
|
|
4396
4758
|
if (!current.entries) {
|
|
4397
4759
|
try {
|
|
4398
4760
|
const target = await fs.resolve(current.dir);
|
|
4399
|
-
|
|
4761
|
+
if (visited.has(target.displayPath))
|
|
4762
|
+
continue;
|
|
4763
|
+
visited.add(target.displayPath);
|
|
4764
|
+
current.entries = (await fs.listDir(target)).slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
4400
4765
|
}
|
|
4401
4766
|
catch {
|
|
4402
|
-
continue;
|
|
4767
|
+
continue;
|
|
4403
4768
|
}
|
|
4404
4769
|
}
|
|
4405
4770
|
let entry;
|
|
@@ -4413,22 +4778,23 @@ async function listFilesDeep(fs, root) {
|
|
|
4413
4778
|
if (!entry)
|
|
4414
4779
|
continue;
|
|
4415
4780
|
if (current.index < current.entries.length)
|
|
4416
|
-
|
|
4417
|
-
const
|
|
4781
|
+
queue.push(current);
|
|
4782
|
+
const path = current.prefix ? `${current.prefix}/${entry.name}` : entry.name;
|
|
4418
4783
|
if (entry.type === 'directory') {
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
index: 0,
|
|
4425
|
-
});
|
|
4784
|
+
if (dirCount >= maxDirectories)
|
|
4785
|
+
continue;
|
|
4786
|
+
out.push({ id: `${path}/`, path: `${path}/`, displayPath: `${path}/`, name: entry.name, kind: 'directory', score: 0 });
|
|
4787
|
+
dirCount += 1;
|
|
4788
|
+
queue.push({ dir: entry.target?.displayPath ?? join(current.dir, entry.name), prefix: path, index: 0 });
|
|
4426
4789
|
}
|
|
4427
4790
|
else if (entry.type === 'file') {
|
|
4428
|
-
|
|
4791
|
+
if (fileCount >= maxFiles)
|
|
4792
|
+
continue;
|
|
4793
|
+
out.push({ id: path, path, displayPath: path, name: entry.name, kind: 'file', score: 0 });
|
|
4794
|
+
fileCount += 1;
|
|
4429
4795
|
}
|
|
4430
4796
|
}
|
|
4431
|
-
return out;
|
|
4797
|
+
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
4432
4798
|
}
|
|
4433
4799
|
/** One attached file's contribution is capped so an absent-minded `@` of a
|
|
4434
4800
|
* huge file cannot blow the context window (CC caps @-attachments too). */
|