@deepseek-harness-tui/dsh-tui 0.8.6 → 0.8.8
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 +15 -5
- package/bin/dsh-tui.js +269 -218
- package/cordis.patch.yml +48 -22
- package/lib/types/cc/figures.d.ts.map +1 -1
- package/lib/types/cc/figures.js +14 -13
- package/lib/types/cc/syntaxRuns.d.ts +16 -0
- package/lib/types/cc/syntaxRuns.d.ts.map +1 -0
- package/lib/types/cc/syntaxRuns.js +79 -0
- package/lib/types/commands.d.ts +14 -0
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +20 -1
- package/lib/types/components/Chat/SubagentMessage.d.ts +19 -0
- package/lib/types/components/Chat/SubagentMessage.d.ts.map +1 -0
- package/lib/types/components/Chat/SubagentMessage.js +75 -0
- package/lib/types/components/EffortInputBorder.d.ts.map +1 -1
- package/lib/types/components/EffortInputBorder.js +2 -2
- package/lib/types/components/EffortTierBadge.js +1 -1
- package/lib/types/components/FileSuggestions.d.ts +8 -4
- package/lib/types/components/FileSuggestions.d.ts.map +1 -1
- package/lib/types/components/FileSuggestions.js +22 -12
- package/lib/types/components/HelpMenu.d.ts +8 -1
- package/lib/types/components/HelpMenu.d.ts.map +1 -1
- package/lib/types/components/HelpMenu.js +15 -4
- package/lib/types/components/LogoV2.d.ts +7 -1
- package/lib/types/components/LogoV2.d.ts.map +1 -1
- package/lib/types/components/LogoV2.js +13 -4
- package/lib/types/components/MessageList.d.ts +6 -2
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +17 -6
- package/lib/types/components/ModelPicker.js +2 -2
- package/lib/types/components/PromptInput.d.ts +1 -1
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +100 -36
- package/lib/types/components/SkillsPicker.js +2 -2
- package/lib/types/components/SplitDiffView.d.ts +1 -13
- package/lib/types/components/SplitDiffView.d.ts.map +1 -1
- package/lib/types/components/SplitDiffView.js +6 -105
- package/lib/types/components/SubagentCard.d.ts +9 -0
- package/lib/types/components/SubagentCard.d.ts.map +1 -0
- package/lib/types/components/SubagentCard.js +26 -0
- package/lib/types/components/SubagentDashboard.d.ts +13 -0
- package/lib/types/components/SubagentDashboard.d.ts.map +1 -0
- package/lib/types/components/SubagentDashboard.js +50 -0
- package/lib/types/components/SubagentDetailScene.d.ts +16 -0
- package/lib/types/components/SubagentDetailScene.d.ts.map +1 -0
- package/lib/types/components/SubagentDetailScene.js +141 -0
- package/lib/types/components/SyntaxText.d.ts +15 -0
- package/lib/types/components/SyntaxText.d.ts.map +1 -0
- package/lib/types/components/SyntaxText.js +34 -0
- package/lib/types/components/ThinkingToggle.d.ts +3 -10
- package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
- package/lib/types/components/ThinkingToggle.js +4 -9
- package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
- package/lib/types/components/design-system/ListItem.js +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +18 -7
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +2 -0
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +42 -6
- package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
- package/lib/types/components/messages/UserPromptMessage.js +15 -2
- package/lib/types/dsh-adapter/channel.d.ts +64 -3
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +387 -37
- package/lib/types/dsh-adapter/contract.d.ts +67 -4
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +149 -15
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +6 -6
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +38 -0
- package/lib/types/dsh-adapter/shortcuts.js +3 -3
- package/lib/types/dsh-adapter/subagents.d.ts +87 -0
- package/lib/types/dsh-adapter/subagents.d.ts.map +1 -0
- package/lib/types/dsh-adapter/subagents.js +204 -0
- package/lib/types/herdr.d.ts +28 -0
- package/lib/types/herdr.d.ts.map +1 -0
- package/lib/types/herdr.js +133 -0
- package/lib/types/i18n.d.ts +233 -32
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +87 -16
- package/lib/types/ink/ink.d.ts +4 -0
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +84 -38
- package/lib/types/ink/render-node-to-output.d.ts +6 -3
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +33 -7
- 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/minimalMode.d.ts +13 -0
- package/lib/types/minimalMode.d.ts.map +1 -0
- package/lib/types/minimalMode.js +17 -0
- package/lib/types/screens/Chat.d.ts +1 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +94 -43
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +6 -2
- package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
- package/lib/types/screens/TrajectoryScene.js +4 -2
- package/lib/types/theme.d.ts +8 -0
- package/lib/types/theme.d.ts.map +1 -1
- package/lib/types/theme.js +38 -14
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +25 -7
- package/lib/types/update.d.ts +18 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +73 -2
- package/lib/types/utils/execFileNoThrow.d.ts +3 -20
- package/lib/types/utils/execFileNoThrow.d.ts.map +1 -1
- package/lib/types/utils/execFileNoThrow.js +53 -32
- package/lib/types/utils/externalEditor.d.ts +12 -11
- package/lib/types/utils/externalEditor.d.ts.map +1 -1
- package/lib/types/utils/externalEditor.js +11 -11
- 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/package.json +81 -74
|
@@ -7,11 +7,12 @@ import { SessionId } from '@deepseek-ai/dsh-session';
|
|
|
7
7
|
import { renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt';
|
|
8
8
|
import { loadBaselineInstructions } from '@deepseek-ai/dsh-agent-instructions';
|
|
9
9
|
import { extname, isAbsolute, join } from 'node:path';
|
|
10
|
-
import { completeCommands, isLocalCommandName, LOCAL_COMMANDS, parseCommandName } from '../commands.js';
|
|
10
|
+
import { completeCommands, HIDDEN_COMMAND_NAMES, isLocalCommandName, LOCAL_COMMANDS, parseCommandName } from '../commands.js';
|
|
11
11
|
import { clearResumeTarget, forgetSession, readResumeTarget, touchSession, writeResumeTarget } from '../sessionHistory.js';
|
|
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';
|
|
@@ -25,8 +26,10 @@ import { extractMentions } from '../utils/mentions.js';
|
|
|
25
26
|
import { t } from '../i18n.js';
|
|
26
27
|
import { modeDisplayName, resolveSessionModes } from '../sessionModes.js';
|
|
27
28
|
import { normalizeStatusBar, normalizeToolBackground } from '../tuiDisplayPrefs.js';
|
|
29
|
+
import { SubagentActivityStore } from './subagents.js';
|
|
28
30
|
import { ActivityTracker } from 'dsh-working-activity/status';
|
|
29
31
|
import { featureOn } from 'dsh-working-activity/config';
|
|
32
|
+
import { setMinimalMode } from '../minimalMode.js';
|
|
30
33
|
import { readActivityConfig } from '../activityPrefs.js';
|
|
31
34
|
import { attachSessionToWorkspace } from './workspace.js';
|
|
32
35
|
import { createLocalWorkspaceRuntime, getHostWorkspaceRuntime } from './workspaces.js';
|
|
@@ -40,6 +43,7 @@ import { installDecisionGuard } from './decision-guard.js';
|
|
|
40
43
|
import { commandOwner } from './command-attribution.js';
|
|
41
44
|
import { readGrantStore } from './grants.js';
|
|
42
45
|
import { hasCommandErrorCode, mapCommandError } from './command-errors.js';
|
|
46
|
+
import { installedMeetsVersion } from './contract.js';
|
|
43
47
|
import { pluginsInfoLines } from './plugins-info.js';
|
|
44
48
|
import { cleanRenderText, cleanScalarText } from './sanitize.js';
|
|
45
49
|
/** `tui/input` return normalization: transform/handled/cancel or no opinion.
|
|
@@ -136,6 +140,20 @@ function normalizeRewindDoneSummary(result, warn) {
|
|
|
136
140
|
* observation (a provider whose directory watcher is still warming).
|
|
137
141
|
*/
|
|
138
142
|
const SKILL_COMMAND_RETRY_MS = 800;
|
|
143
|
+
/** Names the subagent delegation tools ship under (preset `toolName` values
|
|
144
|
+
* plus the CLI default); each renders as a live subagent card, never a plain
|
|
145
|
+
* tool card. */
|
|
146
|
+
const SUBAGENT_TOOL_NAMES = new Set([
|
|
147
|
+
'task',
|
|
148
|
+
'subagent',
|
|
149
|
+
'subagent_fork',
|
|
150
|
+
'subagent_claude_code',
|
|
151
|
+
'subagent_codex',
|
|
152
|
+
'spawn_task',
|
|
153
|
+
]);
|
|
154
|
+
function isSubagentToolName(name) {
|
|
155
|
+
return SUBAGENT_TOOL_NAMES.has(name.toLowerCase());
|
|
156
|
+
}
|
|
139
157
|
const ARGS_PREVIEW_LIMIT = 160;
|
|
140
158
|
const RESULT_PREVIEW_LIMIT = 240;
|
|
141
159
|
/** Local `!`-command output cap (mirrors the result preview limit). */
|
|
@@ -405,6 +423,26 @@ async function waitForTurnEnd(session, fromSeq, timeoutMs) {
|
|
|
405
423
|
export function createChannel(ctx, initialAgent, options) {
|
|
406
424
|
let agent = initialAgent;
|
|
407
425
|
let currentHandle = options.handle;
|
|
426
|
+
const subagentControl = {
|
|
427
|
+
interrupt(agentId) {
|
|
428
|
+
const child = subagentStore.get(agentId);
|
|
429
|
+
const target = child?.sessionId ?? agentId;
|
|
430
|
+
const runtime = ctx.subagents;
|
|
431
|
+
if (!runtime?.interrupt || !target)
|
|
432
|
+
return false;
|
|
433
|
+
try {
|
|
434
|
+
runtime.interrupt(target, { kind: 'ancestor', agent });
|
|
435
|
+
subagentStore.onCancelled(agentId, 'interrupted');
|
|
436
|
+
state.subagents = subagentStore.snapshot();
|
|
437
|
+
syncSubagentRows();
|
|
438
|
+
state.emit();
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
};
|
|
408
446
|
// D-7 backstop: the extensions row installs the decision-subscription
|
|
409
447
|
// gate, but the channel IS the dispatch path — a stale patch without that
|
|
410
448
|
// row (or a bare embed mounting neither) would otherwise leave tui/input
|
|
@@ -418,6 +456,55 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
418
456
|
const fallbackGrantStore = readGrantStore();
|
|
419
457
|
const currentGrantStore = () => ctx.get('tuiPluginHost')?.grants ?? fallbackGrantStore;
|
|
420
458
|
installDecisionGuard(ctx, currentGrantStore());
|
|
459
|
+
// Subagent activity tracking: collects agent/subagent/*, session/event for
|
|
460
|
+
// subagents, and exposes live snapshots for the UI.
|
|
461
|
+
const subagentStore = new SubagentActivityStore();
|
|
462
|
+
// Subagent ChatRow tracking: maps agentId to its ChatRow for live updates.
|
|
463
|
+
const subagentRowsByAgentId = new Map();
|
|
464
|
+
// Task tool descriptions, queued in call order; each subagent/start consumes
|
|
465
|
+
// the oldest one so the card shows the user-visible task label.
|
|
466
|
+
const pendingTaskDescriptions = [];
|
|
467
|
+
/**
|
|
468
|
+
* Sync subagentStore state into ChatRows (insert/update in state.rows).
|
|
469
|
+
* Called whenever subagent state changes (spawned/completed/failed/output).
|
|
470
|
+
*/
|
|
471
|
+
const syncSubagentRows = () => {
|
|
472
|
+
const snapshot = subagentStore.snapshot();
|
|
473
|
+
for (const sub of snapshot) {
|
|
474
|
+
let row = subagentRowsByAgentId.get(sub.agentId);
|
|
475
|
+
if (!row) {
|
|
476
|
+
// New subagent: insert a new ChatRow after the last user or assistant message
|
|
477
|
+
row = {
|
|
478
|
+
id: nextRowId++,
|
|
479
|
+
kind: 'subagent',
|
|
480
|
+
text: sub.description,
|
|
481
|
+
subagent: undefined, // will be filled below
|
|
482
|
+
};
|
|
483
|
+
subagentRowsByAgentId.set(sub.agentId, row);
|
|
484
|
+
state.rows.push(row);
|
|
485
|
+
}
|
|
486
|
+
const subagentRow = {
|
|
487
|
+
agentId: sub.agentId,
|
|
488
|
+
runId: sub.runId,
|
|
489
|
+
description: sub.description,
|
|
490
|
+
provider: sub.provider,
|
|
491
|
+
model: sub.model || 'default',
|
|
492
|
+
effort: sub.effort,
|
|
493
|
+
status: sub.status,
|
|
494
|
+
startedAt: sub.startedAt,
|
|
495
|
+
completedAt: sub.completedAt,
|
|
496
|
+
durationMs: sub.completedAt ? sub.completedAt - sub.startedAt : Date.now() - sub.startedAt,
|
|
497
|
+
outputLines: sub.output.slice(-3),
|
|
498
|
+
toolCalls: sub.toolCalls,
|
|
499
|
+
tokens: sub.tokens,
|
|
500
|
+
summary: sub.summary,
|
|
501
|
+
stopReason: sub.stopReason,
|
|
502
|
+
error: sub.error,
|
|
503
|
+
};
|
|
504
|
+
row.subagent = subagentRow;
|
|
505
|
+
row.text = sub.description;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
421
508
|
// The DSH slash-command registry (optional service): /plan, /goal and
|
|
422
509
|
// friends register here; the TUI merges their descriptors into the slash
|
|
423
510
|
// menu and dispatches through `execute` (which logs the paired
|
|
@@ -686,6 +773,12 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
686
773
|
/** Monotonic token: only the latest `interruptAndDeliver` re-queues, so a
|
|
687
774
|
* second interrupt while the abort settles cannot double-deliver. */
|
|
688
775
|
let interruptSeq = 0;
|
|
776
|
+
// Cancellation is asynchronous: a fast second Esc can arrive after the
|
|
777
|
+
// driver has accepted the first abort but before its turn/end event lands.
|
|
778
|
+
// Do not cancel the same driver twice, or the second cancel can swallow the
|
|
779
|
+
// replacement work queued by interruptAndDeliver and leave the UI gated on
|
|
780
|
+
// a working flag that has not observed turn/end yet.
|
|
781
|
+
let cancelInFlight = false;
|
|
689
782
|
/** The llm runtime seam (dsh-llm LlmRuntime): route metadata resolution. */
|
|
690
783
|
const llmRuntime = ctx.get('llm');
|
|
691
784
|
/** Mutable per-agent model selection (dsh-agent's routing override seam).
|
|
@@ -818,6 +911,17 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
818
911
|
applyEffort(found);
|
|
819
912
|
return true;
|
|
820
913
|
};
|
|
914
|
+
/** Whether the installed command service takes composer images: version
|
|
915
|
+
* gate (composer images arrived on 0.1.0-rc.8 and every later family —
|
|
916
|
+
* 0.1.1 included — keeps the 4-param shape) with a structural fallback,
|
|
917
|
+
* so a failed manifest probe (bundlers, exotic loaders) still lands on
|
|
918
|
+
* the 4-param rc.8 shape at runtime. */
|
|
919
|
+
const commandServiceSupportsImages = (service) => {
|
|
920
|
+
if (installedMeetsVersion('@deepseek-ai/dsh-commands', '0.1.0-rc.8'))
|
|
921
|
+
return true;
|
|
922
|
+
return typeof service.execute?.length === 'number'
|
|
923
|
+
&& service.execute.length >= 4;
|
|
924
|
+
};
|
|
821
925
|
/** Run one DSH registry command (`/plan`, …) on the live agent; the text
|
|
822
926
|
* of its result, '' when the result is textless, undefined when the
|
|
823
927
|
* command is not registered, and the error message when it throws. */
|
|
@@ -864,7 +968,22 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
864
968
|
return t('command-invoke-denied-owner', { name, owner: owner.componentId });
|
|
865
969
|
}
|
|
866
970
|
try {
|
|
867
|
-
const
|
|
971
|
+
const signal = new AbortController().signal;
|
|
972
|
+
const line = `/${name}${rawInput}`;
|
|
973
|
+
const images = await registryCommandImages(commandService, definition, line, signal);
|
|
974
|
+
// rc.8 moved the signal to the 4th parameter and added composer
|
|
975
|
+
// images; older lines (rc.7/rc.6) take (agent, line, signal).
|
|
976
|
+
const execution = images === undefined
|
|
977
|
+
? await commandService.execute(agent, line, signal)
|
|
978
|
+
: await commandService.execute(agent, line, images.images, signal);
|
|
979
|
+
if (images !== undefined && images.dropped.length > 0) {
|
|
980
|
+
// Loud-drop policy mirrors the submit pipeline (mentions-missing):
|
|
981
|
+
// a referenced image that never reached the command must be visible.
|
|
982
|
+
state.notify(t('mentions-missing', { paths: images.dropped.join(' ') }), {
|
|
983
|
+
color: 'warning',
|
|
984
|
+
timeoutMs: 4000,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
868
987
|
// `undefined` = not registered; a handler error surfaces as its
|
|
869
988
|
// message so the user sees why the command failed.
|
|
870
989
|
return execution?.result.text ?? '';
|
|
@@ -873,6 +992,51 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
873
992
|
return error instanceof Error ? error.message : String(error);
|
|
874
993
|
}
|
|
875
994
|
};
|
|
995
|
+
/** Encode the staged `@`-mention images the user pasted for THIS command
|
|
996
|
+
* line into rc.8's `EncodedImageAttachment` payloads; undefined = the
|
|
997
|
+
* installed dsh-commands line predates composer images (rc.7/rc.6), so
|
|
998
|
+
* the caller uses the legacy 3-arg invoke. Matches the submit pipeline's
|
|
999
|
+
* token rule (expandMentions): a staged image attaches only when the
|
|
1000
|
+
* line references its token. A command that does not declare
|
|
1001
|
+
* `input.images` gets NO images — rc.8 admission settles such a batch
|
|
1002
|
+
* as an error, and upstream sends images only to image-capable commands.
|
|
1003
|
+
* A failing read drops just that image (reported via the returned
|
|
1004
|
+
* tokens) while the command still runs. */
|
|
1005
|
+
const registryCommandImages = async (service, definition, line, signal) => {
|
|
1006
|
+
if (!commandServiceSupportsImages(service))
|
|
1007
|
+
return undefined;
|
|
1008
|
+
const declaresImages = definition?.input?.images === true;
|
|
1009
|
+
if (!declaresImages || stagedImages.size === 0)
|
|
1010
|
+
return { images: [], dropped: [] };
|
|
1011
|
+
const store = mentionAttachments(ctx);
|
|
1012
|
+
if (typeof store?.readImage !== 'function')
|
|
1013
|
+
return { images: [], dropped: [] };
|
|
1014
|
+
const images = [];
|
|
1015
|
+
const dropped = [];
|
|
1016
|
+
for (const [token, attachment] of stagedImages) {
|
|
1017
|
+
if (!line.includes(token))
|
|
1018
|
+
continue;
|
|
1019
|
+
try {
|
|
1020
|
+
const stored = await store.readImage(attachment, signal);
|
|
1021
|
+
if (stored?.data instanceof Uint8Array && stored.data.byteLength > 0) {
|
|
1022
|
+
images.push({
|
|
1023
|
+
mediaType: attachment.mediaType,
|
|
1024
|
+
data: Buffer.from(stored.data).toString('base64'),
|
|
1025
|
+
name: attachment.name,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
dropped.push(token);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
catch {
|
|
1033
|
+
// One unreadable staged image is dropped — same loud policy as the
|
|
1034
|
+
// submit pipeline's mentions-missing warning (deliverUserText).
|
|
1035
|
+
dropped.push(token);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return { images, dropped };
|
|
1039
|
+
};
|
|
876
1040
|
// Session-mode folds: last-wins projections over the session log. The
|
|
877
1041
|
// event types are registered by dsh-plan-mode / dsh-sandbox-policy /
|
|
878
1042
|
// dsh-user-approval and are NOT in this package's typed SessionEvent
|
|
@@ -967,6 +1131,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
967
1131
|
const index = deriveModeIndex(agent.session.events);
|
|
968
1132
|
await applyMode(sessionModes[(index + 1) % sessionModes.length]);
|
|
969
1133
|
};
|
|
1134
|
+
// Session-lifetime candidate pool for non-path queries. The load promise is
|
|
1135
|
+
// shared so concurrent first keystrokes cannot kick off duplicate scans, and
|
|
1136
|
+
// it is keyed by cwd so a /workspace switch or resumed session never reuses
|
|
1137
|
+
// another directory's listing.
|
|
1138
|
+
const fileCandidateCache = { cwd: '', load: undefined };
|
|
970
1139
|
const state = {
|
|
971
1140
|
effortLevels: undefined,
|
|
972
1141
|
version: 0,
|
|
@@ -1001,6 +1170,8 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1001
1170
|
thinkingFold: options.thinkingFold ?? 'preview',
|
|
1002
1171
|
toolBackground: normalizeToolBackground(options.toolBackground),
|
|
1003
1172
|
statusBar: normalizeStatusBar(options.statusBar),
|
|
1173
|
+
whale: options.whale !== false,
|
|
1174
|
+
minimal: options.minimal === true,
|
|
1004
1175
|
activityEnabled: options.activity !== false,
|
|
1005
1176
|
contextBarEnabled: options.contextBar !== false,
|
|
1006
1177
|
agentPreset: options.agentPreset,
|
|
@@ -1042,6 +1213,8 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1042
1213
|
thinking: 0,
|
|
1043
1214
|
tools: 0,
|
|
1044
1215
|
},
|
|
1216
|
+
subagents: [],
|
|
1217
|
+
subagentControl,
|
|
1045
1218
|
subscribe(listener) {
|
|
1046
1219
|
listeners.add(listener);
|
|
1047
1220
|
return () => {
|
|
@@ -1165,6 +1338,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1165
1338
|
cancel() {
|
|
1166
1339
|
// Keep the staged queue: an interrupt aborts the running turn but the
|
|
1167
1340
|
// queued/steered messages are delivered as the next turn (web parity).
|
|
1341
|
+
// Cancellation converges asynchronously; ignore a repeated Esc/Ctrl+C
|
|
1342
|
+
// until the aborted turn has produced its terminal event.
|
|
1343
|
+
if (cancelInFlight)
|
|
1344
|
+
return;
|
|
1345
|
+
cancelInFlight = true;
|
|
1168
1346
|
agent.cancel({ kind: 'user' }, { keepInbox: true });
|
|
1169
1347
|
},
|
|
1170
1348
|
interruptAndDeliver(texts) {
|
|
@@ -1176,8 +1354,13 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1176
1354
|
// followup. dsh-agent's cancel-convergence wake latch accepts this
|
|
1177
1355
|
// wake immediately after cancel and starts it once the aborted turn
|
|
1178
1356
|
// retires; waiting for whenIdle is unsafe because it also follows
|
|
1179
|
-
// replacement work and may never settle.
|
|
1180
|
-
|
|
1357
|
+
// replacement work and may never settle. If cancellation is already
|
|
1358
|
+
// in flight, keep the existing abort and still replace the pending
|
|
1359
|
+
// interrupt delivery; fake/embedded agents may not emit turn/end.
|
|
1360
|
+
if (!cancelInFlight) {
|
|
1361
|
+
cancelInFlight = true;
|
|
1362
|
+
agent.cancel({ kind: 'user' });
|
|
1363
|
+
}
|
|
1181
1364
|
const token = ++interruptSeq;
|
|
1182
1365
|
const deliver = () => {
|
|
1183
1366
|
// A second interrupt while the abort is still settling must not
|
|
@@ -1985,6 +2168,19 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1985
2168
|
state.statusBar = next;
|
|
1986
2169
|
state.emit();
|
|
1987
2170
|
},
|
|
2171
|
+
setWhale(visible) {
|
|
2172
|
+
if (visible === state.whale)
|
|
2173
|
+
return;
|
|
2174
|
+
state.whale = visible;
|
|
2175
|
+
state.emit();
|
|
2176
|
+
},
|
|
2177
|
+
setMinimal(enabled) {
|
|
2178
|
+
setMinimalMode(enabled);
|
|
2179
|
+
if (enabled === state.minimal)
|
|
2180
|
+
return;
|
|
2181
|
+
state.minimal = enabled;
|
|
2182
|
+
state.emit();
|
|
2183
|
+
},
|
|
1988
2184
|
setActivityFrames(name) {
|
|
1989
2185
|
if (!isPresetName(name)) {
|
|
1990
2186
|
state.notify(t('unknown-activity-preset', { name }), { color: 'error' });
|
|
@@ -2098,13 +2294,15 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2098
2294
|
// snapshot() over list(): only a COMPLETE observation is authoritative
|
|
2099
2295
|
// (same contract as the skill-command merge above) — a partial catalog
|
|
2100
2296
|
// must surface as "failed", not as a misleading near-empty picker.
|
|
2101
|
-
const
|
|
2297
|
+
const target = agent;
|
|
2298
|
+
const registry = skillRegistryFor(target);
|
|
2102
2299
|
if (registry === undefined)
|
|
2103
2300
|
return [];
|
|
2104
2301
|
try {
|
|
2105
|
-
const observation = await registry.snapshot(skillViewOptions(
|
|
2106
|
-
if (!observation.complete)
|
|
2302
|
+
const observation = await registry.snapshot(skillViewOptions(target));
|
|
2303
|
+
if (target !== agent || !observation.complete) {
|
|
2107
2304
|
return undefined;
|
|
2305
|
+
}
|
|
2108
2306
|
return observation.skills.map(skill => ({
|
|
2109
2307
|
name: skill.name,
|
|
2110
2308
|
description: skill.description,
|
|
@@ -2269,9 +2467,33 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2269
2467
|
signal: options?.signal,
|
|
2270
2468
|
});
|
|
2271
2469
|
},
|
|
2272
|
-
|
|
2470
|
+
async listFileCandidates(query, options) {
|
|
2471
|
+
const fs = ctx.get('fs');
|
|
2472
|
+
if (!fs || options?.signal?.aborted)
|
|
2473
|
+
return [];
|
|
2474
|
+
if (isPathLikeQuery(query)) {
|
|
2475
|
+
return listPathCandidates(fs, state.cwd, query, options?.signal, options?.topK ?? 50);
|
|
2476
|
+
}
|
|
2477
|
+
if (fileCandidateCache.cwd !== state.cwd) {
|
|
2478
|
+
fileCandidateCache.cwd = state.cwd;
|
|
2479
|
+
fileCandidateCache.load = undefined;
|
|
2480
|
+
}
|
|
2481
|
+
fileCandidateCache.load ??= listFilesDeepCandidates(fs, state.cwd).then(candidates => {
|
|
2482
|
+
if (candidates.length > 0)
|
|
2483
|
+
return candidates;
|
|
2484
|
+
// An empty scan is not worth caching forever — retry on next query.
|
|
2485
|
+
fileCandidateCache.load = undefined;
|
|
2486
|
+
return candidates;
|
|
2487
|
+
});
|
|
2488
|
+
const candidates = await fileCandidateCache.load;
|
|
2489
|
+
if (options?.signal?.aborted)
|
|
2490
|
+
return [];
|
|
2491
|
+
return rankFileCandidates(candidates, query, options?.topK ?? 50);
|
|
2492
|
+
},
|
|
2493
|
+
async listFiles() {
|
|
2273
2494
|
const fs = ctx.get('fs');
|
|
2274
|
-
|
|
2495
|
+
const candidates = await listFilesDeepCandidates(fs, state.cwd);
|
|
2496
|
+
return candidates.map(candidate => candidate.path);
|
|
2275
2497
|
},
|
|
2276
2498
|
async listSessions() {
|
|
2277
2499
|
// Every stored session, classified and unfiltered. Which of them a
|
|
@@ -2717,6 +2939,10 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2717
2939
|
const merged = [...LOCAL_COMMANDS];
|
|
2718
2940
|
if (commandService) {
|
|
2719
2941
|
for (const descriptor of commandService.list(target)) {
|
|
2942
|
+
// Hidden TUI commands (e.g. /deepseek) stay out of the public
|
|
2943
|
+
// command catalog even if a plugin/skill happens to share the name.
|
|
2944
|
+
if (HIDDEN_COMMAND_NAMES.has(descriptor.name))
|
|
2945
|
+
continue;
|
|
2720
2946
|
if (merged.some(command => command.name === descriptor.name))
|
|
2721
2947
|
continue;
|
|
2722
2948
|
const descriptions = commandTrees?.descriptions(descriptor.name);
|
|
@@ -3559,6 +3785,22 @@ ${output}
|
|
|
3559
3785
|
// no card is a no-op below.
|
|
3560
3786
|
if (event.data.name === 'ask_user_question')
|
|
3561
3787
|
break;
|
|
3788
|
+
// The Task tool's plain card is replaced by the live subagent card
|
|
3789
|
+
// (Kimi Code semantics): the delegation itself renders as a subagent
|
|
3790
|
+
// row, so the raw args/result card would only duplicate it. The call
|
|
3791
|
+
// still runs - only its transcript rendering is suppressed.
|
|
3792
|
+
if (isSubagentToolName(event.data.name)) {
|
|
3793
|
+
try {
|
|
3794
|
+
const args = JSON.parse(event.data.arguments);
|
|
3795
|
+
if (typeof args.description === 'string' && args.description)
|
|
3796
|
+
pendingTaskDescriptions.push(args.description);
|
|
3797
|
+
}
|
|
3798
|
+
catch {
|
|
3799
|
+
// Unparseable args leave the queue untouched; the card falls back
|
|
3800
|
+
// to the provider label.
|
|
3801
|
+
}
|
|
3802
|
+
break;
|
|
3803
|
+
}
|
|
3562
3804
|
// Reasoning that led to a tool call is done thinking — fold the
|
|
3563
3805
|
// preview now, before the tool card grows the transcript past it
|
|
3564
3806
|
// (see foldLiveReasoning).
|
|
@@ -3629,6 +3871,7 @@ ${output}
|
|
|
3629
3871
|
break;
|
|
3630
3872
|
}
|
|
3631
3873
|
case 'turn/start': {
|
|
3874
|
+
cancelInFlight = false;
|
|
3632
3875
|
state.working = true;
|
|
3633
3876
|
state.turnStart = Date.now();
|
|
3634
3877
|
state.responseChars = 0;
|
|
@@ -3644,6 +3887,7 @@ ${output}
|
|
|
3644
3887
|
break;
|
|
3645
3888
|
}
|
|
3646
3889
|
case 'turn/end': {
|
|
3890
|
+
cancelInFlight = false;
|
|
3647
3891
|
settleStreaming();
|
|
3648
3892
|
state.working = false;
|
|
3649
3893
|
state.activeToolCount = 0;
|
|
@@ -3910,6 +4154,19 @@ ${output}
|
|
|
3910
4154
|
};
|
|
3911
4155
|
})(),
|
|
3912
4156
|
ctx.on('session/event', (session, event) => {
|
|
4157
|
+
// First check if this is a subagent session
|
|
4158
|
+
const subagentId = subagentStore.getSubagentIdBySession(session);
|
|
4159
|
+
if (subagentId) {
|
|
4160
|
+
subagentStore.onSessionEvent(subagentId, event);
|
|
4161
|
+
state.subagents = subagentStore.snapshot();
|
|
4162
|
+
syncSubagentRows();
|
|
4163
|
+
if (event.type === 'assistant/chunk')
|
|
4164
|
+
state.emitStream();
|
|
4165
|
+
else
|
|
4166
|
+
state.emit();
|
|
4167
|
+
return;
|
|
4168
|
+
}
|
|
4169
|
+
// Otherwise handle main agent session
|
|
3913
4170
|
if (session !== agent.session)
|
|
3914
4171
|
return;
|
|
3915
4172
|
// Observation broker (C-042): maps user/message + assistant/message
|
|
@@ -3940,6 +4197,67 @@ ${output}
|
|
|
3940
4197
|
else
|
|
3941
4198
|
state.emit();
|
|
3942
4199
|
}),
|
|
4200
|
+
// Subagent lifecycle tracking. The dsh-subagent service publishes scoped
|
|
4201
|
+
// observe-only events as `subagent/start` and `subagent/end`; the parent
|
|
4202
|
+
// Agent is carried by Cordis scope dispatch, not included in the payload.
|
|
4203
|
+
(() => {
|
|
4204
|
+
const disposeStart = ctx.on('subagent/start', (info) => {
|
|
4205
|
+
if (!info?.id)
|
|
4206
|
+
return;
|
|
4207
|
+
subagentStore.onSpawned(info.id, info.provider || 'subagent', info.provider, {
|
|
4208
|
+
runId: info.runId ?? info.id,
|
|
4209
|
+
local: info.local,
|
|
4210
|
+
description: pendingTaskDescriptions.shift() ?? `${info.provider || 'subagent'} task`,
|
|
4211
|
+
});
|
|
4212
|
+
// In-process providers publish a child Agent during this notification.
|
|
4213
|
+
// Resolve through ctx.get('agents') (the property proxy is
|
|
4214
|
+
// topology-sensitive); the child carries its session (live output
|
|
4215
|
+
// stream) and its provider/model route for the card header.
|
|
4216
|
+
try {
|
|
4217
|
+
const agents = ctx.get('agents');
|
|
4218
|
+
const child = agents?.get(info.id);
|
|
4219
|
+
if (child?.session) {
|
|
4220
|
+
subagentStore.linkSession(info.id, child.session);
|
|
4221
|
+
const model = child.options?.model ?? child.options?.provider;
|
|
4222
|
+
if (model)
|
|
4223
|
+
subagentStore.patch(info.id, { model, provider: child.options?.provider ?? info.provider });
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
catch {
|
|
4227
|
+
// Session discovery is best-effort and must not break the parent turn.
|
|
4228
|
+
}
|
|
4229
|
+
state.subagents = subagentStore.snapshot();
|
|
4230
|
+
syncSubagentRows();
|
|
4231
|
+
state.emit();
|
|
4232
|
+
});
|
|
4233
|
+
const disposeEnd = ctx.on('subagent/end', (info) => {
|
|
4234
|
+
if (!info?.id)
|
|
4235
|
+
return;
|
|
4236
|
+
const output = Array.isArray(info.lastAssistantMessage)
|
|
4237
|
+
? info.lastAssistantMessage
|
|
4238
|
+
.map(block => typeof block === 'object' && block !== null && 'text' in block ? String(block.text ?? '') : '')
|
|
4239
|
+
.filter(Boolean)
|
|
4240
|
+
.join('\n')
|
|
4241
|
+
: '';
|
|
4242
|
+
// The final assistant output becomes the card's summary only; the
|
|
4243
|
+
// running waterfall came from the child session stream, so echoing
|
|
4244
|
+
// it into the output buffer would duplicate it on the collapsed card.
|
|
4245
|
+
subagentStore.flushOutput(info.id);
|
|
4246
|
+
if (info.stopReason === 'completed')
|
|
4247
|
+
subagentStore.onCompleted(info.id, output, info.stopReason);
|
|
4248
|
+
else if (info.stopReason === 'cancelled' || info.stopReason === 'aborted')
|
|
4249
|
+
subagentStore.onCancelled(info.id, info.stopReason, output);
|
|
4250
|
+
else
|
|
4251
|
+
subagentStore.onFailed(info.id, info.stopReason || 'Unknown error');
|
|
4252
|
+
state.subagents = subagentStore.snapshot();
|
|
4253
|
+
syncSubagentRows();
|
|
4254
|
+
state.emit();
|
|
4255
|
+
});
|
|
4256
|
+
return () => {
|
|
4257
|
+
disposeStart();
|
|
4258
|
+
disposeEnd();
|
|
4259
|
+
};
|
|
4260
|
+
})(),
|
|
3943
4261
|
];
|
|
3944
4262
|
};
|
|
3945
4263
|
// Subagents inherit provider/model from AgentOptions, but resumed TUI
|
|
@@ -4086,33 +4404,64 @@ function usageOutputTokens(usage) {
|
|
|
4086
4404
|
? value
|
|
4087
4405
|
: undefined;
|
|
4088
4406
|
}
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4407
|
+
async function listPathCandidates(fs, cwd, query, signal, topK) {
|
|
4408
|
+
const normalized = query.replaceAll('\\', '/');
|
|
4409
|
+
const slash = normalized.lastIndexOf('/');
|
|
4410
|
+
// `.` / `..` without a trailing separator are whole-directory queries too.
|
|
4411
|
+
const bareDir = slash < 0 && (normalized === '.' || normalized === '..' || normalized === '~');
|
|
4412
|
+
const directoryPart = slash < 0 ? (bareDir ? `${normalized}/` : '') : normalized.slice(0, slash + 1);
|
|
4413
|
+
const nameQuery = slash < 0 || bareDir ? '' : normalized.slice(slash + 1);
|
|
4414
|
+
// `~/` expands against the host home (matches the cwd resolution rules);
|
|
4415
|
+
// drive-letter and POSIX-absolute prefixes pass through untouched.
|
|
4416
|
+
const expanded = directoryPart === '~/'
|
|
4417
|
+
? `${homeDir()}/`
|
|
4418
|
+
: directoryPart.startsWith('/') || /^[A-Za-z]:\//.test(directoryPart)
|
|
4419
|
+
? directoryPart
|
|
4420
|
+
: join(cwd, directoryPart || '.');
|
|
4421
|
+
try {
|
|
4422
|
+
if (signal?.aborted)
|
|
4423
|
+
return [];
|
|
4424
|
+
const target = await fs.resolve(expanded);
|
|
4425
|
+
const entries = (await fs.listDir(target)).slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
4426
|
+
return rankFileCandidates(entries.filter(entry => entry.type === 'file' || entry.type === 'directory').map(entry => {
|
|
4427
|
+
const path = `${directoryPart}${entry.name}${entry.type === 'directory' ? '/' : ''}`;
|
|
4428
|
+
return { id: path, path, displayPath: path, name: entry.name, kind: entry.type, score: 0 };
|
|
4429
|
+
}), nameQuery, topK);
|
|
4430
|
+
}
|
|
4431
|
+
catch {
|
|
4432
|
+
return [];
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
async function listFilesDeepCandidates(fs, root, signal) {
|
|
4098
4436
|
if (!fs)
|
|
4099
4437
|
return [];
|
|
4100
4438
|
const out = [];
|
|
4101
4439
|
const SKIP = new Set(['node_modules', '.git', '.hg', '.svn', '.DS_Store', 'dist']);
|
|
4102
4440
|
const BUILD_DIR = /^(?:build(?:[-_].*)?|cmake-build(?:[-_].*)?)$/i;
|
|
4103
|
-
const
|
|
4104
|
-
const
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4441
|
+
const queue = [{ dir: root, prefix: '', index: 0 }];
|
|
4442
|
+
const visited = new Set();
|
|
4443
|
+
const maxFiles = 100;
|
|
4444
|
+
const maxDirectories = 100;
|
|
4445
|
+
let fileCount = 0;
|
|
4446
|
+
let dirCount = 0;
|
|
4447
|
+
// Round-robin: each directory yields ONE non-skipped entry per visit before
|
|
4448
|
+
// it re-queues, so a large early sibling (e.g. `generated/` with 120 files)
|
|
4449
|
+
// cannot starve `src/` out of the per-kind budgets. This is the regression
|
|
4450
|
+
// contract pinned by scripts/verify-file-completion.mjs.
|
|
4451
|
+
while (queue.length && fileCount < maxFiles && dirCount < maxDirectories) {
|
|
4452
|
+
if (signal?.aborted)
|
|
4453
|
+
return [];
|
|
4454
|
+
const current = queue.shift();
|
|
4109
4455
|
if (!current.entries) {
|
|
4110
4456
|
try {
|
|
4111
4457
|
const target = await fs.resolve(current.dir);
|
|
4112
|
-
|
|
4458
|
+
if (visited.has(target.displayPath))
|
|
4459
|
+
continue;
|
|
4460
|
+
visited.add(target.displayPath);
|
|
4461
|
+
current.entries = (await fs.listDir(target)).slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
4113
4462
|
}
|
|
4114
4463
|
catch {
|
|
4115
|
-
continue;
|
|
4464
|
+
continue;
|
|
4116
4465
|
}
|
|
4117
4466
|
}
|
|
4118
4467
|
let entry;
|
|
@@ -4126,22 +4475,23 @@ async function listFilesDeep(fs, root) {
|
|
|
4126
4475
|
if (!entry)
|
|
4127
4476
|
continue;
|
|
4128
4477
|
if (current.index < current.entries.length)
|
|
4129
|
-
|
|
4130
|
-
const
|
|
4478
|
+
queue.push(current);
|
|
4479
|
+
const path = current.prefix ? `${current.prefix}/${entry.name}` : entry.name;
|
|
4131
4480
|
if (entry.type === 'directory') {
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
index: 0,
|
|
4138
|
-
});
|
|
4481
|
+
if (dirCount >= maxDirectories)
|
|
4482
|
+
continue;
|
|
4483
|
+
out.push({ id: `${path}/`, path: `${path}/`, displayPath: `${path}/`, name: entry.name, kind: 'directory', score: 0 });
|
|
4484
|
+
dirCount += 1;
|
|
4485
|
+
queue.push({ dir: entry.target?.displayPath ?? join(current.dir, entry.name), prefix: path, index: 0 });
|
|
4139
4486
|
}
|
|
4140
4487
|
else if (entry.type === 'file') {
|
|
4141
|
-
|
|
4488
|
+
if (fileCount >= maxFiles)
|
|
4489
|
+
continue;
|
|
4490
|
+
out.push({ id: path, path, displayPath: path, name: entry.name, kind: 'file', score: 0 });
|
|
4491
|
+
fileCount += 1;
|
|
4142
4492
|
}
|
|
4143
4493
|
}
|
|
4144
|
-
return out;
|
|
4494
|
+
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
4145
4495
|
}
|
|
4146
4496
|
/** One attached file's contribution is capped so an absent-minded `@` of a
|
|
4147
4497
|
* huge file cannot blow the context window (CC caps @-attachments too). */
|