@toddzheng024/dscode-bundle 0.7.25 → 0.7.27

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.
Files changed (40) hide show
  1. package/package.json +2 -1
  2. package/plugins/openrouter/wire.mjs +3 -0
  3. package/plugins/session-metrics/index.mjs +16 -7
  4. package/plugins/session-metrics/rate.mjs +26 -61
  5. package/plugins/session-metrics/view.mjs +26 -12
  6. package/plugins/triggers/cli.mjs +155 -36
  7. package/plugins/triggers/commands.mjs +140 -0
  8. package/plugins/triggers/config.mjs +32 -13
  9. package/plugins/triggers/host.mjs +28 -21
  10. package/plugins/triggers/index.mjs +17 -47
  11. package/plugins/triggers/job-cli.mjs +110 -0
  12. package/plugins/triggers/jobs.mjs +156 -0
  13. package/plugins/triggers/lease.mjs +24 -0
  14. package/plugins/triggers/management.mjs +181 -0
  15. package/plugins/triggers/options.mjs +8 -0
  16. package/plugins/triggers/poll.mjs +12 -4
  17. package/plugins/triggers/run.mjs +1 -0
  18. package/plugins/triggers/schedule.mjs +40 -0
  19. package/plugins/triggers/scheduler-service.mjs +46 -0
  20. package/plugins/triggers/scheduler.mjs +90 -0
  21. package/plugins/triggers/session.mjs +43 -0
  22. package/plugins/triggers/source-emit.mjs +19 -0
  23. package/plugins/triggers/source-host.mjs +78 -0
  24. package/plugins/triggers/source-ingress.mjs +70 -0
  25. package/plugins/triggers/source-sandbox.mjs +35 -0
  26. package/plugins/triggers/sources.mjs +45 -0
  27. package/plugins/triggers/spool.mjs +23 -6
  28. package/plugins/triggers/tools.mjs +74 -0
  29. package/vendor/tui/lib/app.mjs +66 -87
  30. package/vendor/tui/lib/dscode/preset.mjs +18 -0
  31. package/vendor/tui/lib/dscode/telemetry.mjs +25 -9
  32. package/vendor/tui/lib/index.mjs +22 -78
  33. package/vendor/tui/lib/kernel-panels.mjs +3 -2
  34. package/vendor/tui/lib/locales/en.mjs +3 -2
  35. package/vendor/tui/lib/locales/zh.mjs +3 -2
  36. package/vendor/tui/lib/models.mjs +8 -0
  37. package/vendor/tui/lib/render/inspector.mjs +1 -1
  38. package/vendor/tui/lib/render/projection.mjs +11 -6
  39. package/vendor/tui/lib/render/status.mjs +39 -10
  40. package/vendor/tui/lib/startup.mjs +4 -5
@@ -30,7 +30,7 @@ import { imeCursorRowsUp, useImeCursorAnchor } from './render/ime-cursor.mjs';
30
30
  import { readClipboardImage } from './dscode/clipboard-image/index.mjs';
31
31
  import { createChatLinesCache, dscodeChatLines } from './dscode/chat.mjs';
32
32
  import { readFileSync } from 'node:fs';
33
- import { FOOTER_FIGURE_RESERVE, footerFor as dscodeFooterFor } from '../../../plugins/session-metrics/view.mjs';
33
+ import { footerFiguresFor as dscodeFooterFiguresFor } from '../../../plugins/session-metrics/view.mjs';
34
34
  import { newerVersion as dscodeNewerVersion } from '../../../plugins/tui-tools/update.mjs';
35
35
  import { languageName as dscodeLanguageName, normalizeLanguage as dscodeNormalizeLanguage, t as dscodeMessage } from '../../../plugins/i18n/messages.mjs';
36
36
  import { dscodeTelemetryNodes } from './dscode/telemetry.mjs';
@@ -89,10 +89,11 @@ import { visibleColumns } from './render/markdown.mjs';
89
89
  import { busyChaseFrame, BUSY_CHASE_TICK_MS, CARET_BLINK_TICK_MS, caretVisible, DEEP_DIVING_SHIMMER_TICK_MS, DEEPSEEK_WAVE_TICK_MS, deepseekWaveColumnBg, deepseekWaveDuration, deepseekWaveSpark, deepseekWaveStyleRandom, deepseekWaveTier, deepseekWaveWordHue, deepseekWaveWordVisible, deepDivingGradientColor, deepDivingSparkColor, flowColor, effortAboveHigh, isOfficialDeepSeekLabel, parseAnimationsArgument, RAINBOW_BURST_DURATION_MS, RAINBOW_BURST_TICK_MS, rainbowBurstColumnBg, rainbowSpectrumHue, } from './render/animations.mjs';
90
90
  import { isSlashLine, submissionPayload } from './commands.mjs';
91
91
  import { rankByName } from './render/fuzzy.mjs';
92
+ import { resolveModelEffort } from './models.mjs';
92
93
  import { isDeclaredReasoningEfforts, parseReasoningEffortsDraft, serializeReasoningEfforts, } from './provider-settings.mjs';
93
94
  import { isPathLikeMentionQuery } from './mentions.mjs';
94
95
  import { createTranscriptStore } from './store.mjs';
95
- import { AgentsPanel, editQuery, EffortPanel as NativeEffortPanel, HistoryPanel, JobsPanel, ModePanel, PermissionPanel, PluginPanel, ResumePanel, ReviewPickerPanel, SchedulePanel, SearchPanel, StatuslinePanel, runClock, SubagentPanel, UsagePanel } from './kernel-panels.mjs';
96
+ import { AgentsPanel, editQuery, EffortPanel as NativeEffortPanel, HistoryPanel, JobsPanel, PermissionPanel, PluginPanel, ResumePanel, ReviewPickerPanel, SchedulePanel, SearchPanel, StatuslinePanel, runClock, SubagentPanel, UsagePanel } from './kernel-panels.mjs';
96
97
  import { beginRecall, recallEntries, recallNewer, recallOlder, recordLocalEntry, } from './history.mjs';
97
98
  import { authorizationForProvider, providerAuthorizationStatus, } from './authorization.mjs';
98
99
  import { ProviderAuthorizationLogoutPanel, ProviderAuthorizationPanel } from './authorization-panel.mjs';
@@ -137,7 +138,6 @@ const LOCAL_COMMANDS = [
137
138
  { label: '/help', descriptionKey: 'cmd.help' },
138
139
  { label: '/model', descriptionKey: 'cmd.model' },
139
140
  { label: '/effort', descriptionKey: 'cmd.effort' },
140
- { label: '/mode', descriptionKey: 'cmd.mode' },
141
141
  { label: '/permission', descriptionKey: 'cmd.permission' },
142
142
  { label: '/new', descriptionKey: 'cmd.new' },
143
143
  { label: '/fork', descriptionKey: 'cmd.fork' },
@@ -179,7 +179,7 @@ const LOCAL_COMMANDS = [
179
179
  { label: '/review', descriptionKey: 'cmd.review' },
180
180
  { label: '/quit', descriptionKey: 'cmd.quit' },
181
181
  ];
182
- const LOCAL_COMMAND_NAMES = new Set(LOCAL_COMMANDS.map(command => command.label.slice(1)));
182
+ const LOCAL_COMMAND_NAMES = new Set([...LOCAL_COMMANDS.map(command => command.label.slice(1)), 'mode']);
183
183
  /** Pad text with spaces to a visible-column target (menu name column). */
184
184
  function padColumns(text, width) {
185
185
  const clipped = truncateColumns(singleLineText(text), width);
@@ -990,13 +990,13 @@ function statusToneProps(tone, flowMs) {
990
990
  }
991
991
  }
992
992
  /**
993
- * The footer status line: two stacked physical rows in every mode. Row 1
993
+ * The footer status line: two rows, or three when finances need their own row. Row 1
994
994
  * carries Claude-Code-style identity facts and session figures from the left
995
995
  * with the Codex-style permission badge — the autonomous-selection anchor
996
996
  * with its shift+tab cycle hint — pinned to the right edge. Row 2 (mode,
997
997
  * context progress bar, cache, duration figures) renders only while it has
998
- * content, so the footer degrades to a single row on narrow terminals. Both
999
- * layouts arrive pre-measured from the pure reducer, so Ink only paints;
998
+ * content, and the financial group moves to row 3 when needed. All rows
999
+ * arrive pre-measured from the pure reducer, so Ink only paints;
1000
1000
  * truncation degrades groups, it never wraps a row.
1001
1001
  *
1002
1002
  * The DeepSeek easter egg: when the model label *switches* to an official
@@ -1038,18 +1038,11 @@ export function StatusLine({ facts, stats, busy, columns, items, onRows, animate
1038
1038
  const timer = setInterval(() => dscodeRefreshMetrics(n => n + 1), 1000);
1039
1039
  return () => clearInterval(timer);
1040
1040
  }, []);
1041
- // dscode: the live figures close row 2 as one left-hand cluster and only appear
1042
- // once the terminal can seat them; row 1 names the model itself. The cluster is
1043
- // laid out against the same width budget as before, and its own ladder decides
1044
- // which figures fit.
1045
- const telemetryWidth = Math.max(1, Math.min(columns - 8, Math.max(40, Math.floor(columns * 0.8) - 4) + FOOTER_FIGURE_RESERVE));
1046
- // The figures belong to the provider serving the route (`provider/model`).
1041
+ // Keep the figures structured: the layout can move the complete financial
1042
+ // group to a third row instead of dropping it to make the left group fit.
1047
1043
  const slash = facts.model.indexOf('/');
1048
1044
  const provider = slash > 0 ? facts.model.slice(0, slash) : 'deepseek-official';
1049
- const telemetry = columns >= 48
1050
- ? dscodeFooterFor(facts.fullSessionId, stats, telemetryWidth, provider, getLanguage())
1051
- : '';
1052
- facts = { ...facts, telemetry };
1045
+ facts = { ...facts, telemetryFigures: dscodeFooterFiguresFor(facts.fullSessionId, stats, provider, getLanguage()) };
1053
1046
  // Flowing-theme busy flow: the identity cluster's live dot cycles the
1054
1047
  // anchor walk while a turn runs; static themes never start the timer.
1055
1048
  const flow = themeFlow();
@@ -1065,6 +1058,12 @@ export function StatusLine({ facts, stats, busy, columns, items, onRows, animate
1065
1058
  contextWidth: Math.max(5, columns - 6),
1066
1059
  }), [
1067
1060
  facts.telemetry,
1061
+ facts.telemetryFigures?.current,
1062
+ facts.telemetryFigures?.average,
1063
+ facts.telemetryFigures?.context,
1064
+ facts.telemetryFigures?.money,
1065
+ facts.telemetryFigures?.cache,
1066
+ facts.telemetryFigures?.turn,
1068
1067
  facts.model,
1069
1068
  facts.effort,
1070
1069
  facts.mode,
@@ -1089,7 +1088,7 @@ export function StatusLine({ facts, stats, busy, columns, items, onRows, animate
1089
1088
  // The IME anchor below the composer counts every row between the caret and
1090
1089
  // Ink's parked cursor, so the footer reports its exact row count one-way
1091
1090
  // (same contract as the composer's row report).
1092
- const statusRowCount = layout.row2.left.length > 0 || layout.row2.right.length > 0 ? 2 : 1;
1091
+ const statusRowCount = layout.row3 !== undefined ? 3 : layout.row2.left.length > 0 || layout.row2.right.length > 0 ? 2 : 1;
1093
1092
  useEffect(() => {
1094
1093
  onRows?.(statusRowCount);
1095
1094
  }, [onRows, statusRowCount]);
@@ -1099,10 +1098,10 @@ export function StatusLine({ facts, stats, busy, columns, items, onRows, animate
1099
1098
  if (groupIndex > 0) {
1100
1099
  leftParts.push(createElement(Text, { key: key + 'gs' + groupIndex, color: inkColor(getPalette().dim) }, STATUS_GROUP_SEPARATOR));
1101
1100
  }
1102
- const telemetryGroup = group.id === 'telemetry';
1101
+ const telemetryGroup = group.id?.startsWith('telemetry') === true;
1103
1102
  group.spans.forEach((span, spanIndex) => {
1104
1103
  const spanKey = key + 'g' + groupIndex + 's' + spanIndex;
1105
- leftParts.push(createElement(Text, { key: spanKey, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) }, telemetryGroup ? dscodeTelemetryNodes(span.text, spanKey) : span.text));
1104
+ leftParts.push(createElement(Text, { key: spanKey, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) }, telemetryGroup ? dscodeTelemetryNodes(span.text, spanKey, animated) : span.text));
1106
1105
  });
1107
1106
  });
1108
1107
  const rightParts = [];
@@ -1119,7 +1118,7 @@ export function StatusLine({ facts, stats, busy, columns, items, onRows, animate
1119
1118
  if (index > 0) {
1120
1119
  rightParts.push(createElement(Text, { key: key + 'rs' + index, color: inkColor(getPalette().dim) }, STATUS_ITEM_SEPARATOR));
1121
1120
  }
1122
- rightParts.push(createElement(Text, { key: key + 'r' + index, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) }, span.text));
1121
+ rightParts.push(createElement(Text, { key: key + 'r' + index, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) }, span.telemetry ? dscodeTelemetryNodes(span.text, key + 'r' + index, animated) : span.text));
1123
1122
  });
1124
1123
  // Each row already fits the column budget; truncate-end stays as the
1125
1124
  // terminal-measurement backstop so a drifting cell count clips instead
@@ -1129,10 +1128,10 @@ export function StatusLine({ facts, stats, busy, columns, items, onRows, animate
1129
1128
  // The secondary row adds the model-name indent
1130
1129
  // (its budget already shrinks by the same amount) so its figures align
1131
1130
  // under the model name rather than under the busy dot.
1132
- { paddingLeft: 2 + indent, width: columns, justifyContent: rightParts.length > 0 ? 'space-between' : undefined }, createElement(Text, { wrap: 'truncate-end' }, ...leftParts), rightParts.length > 0 ? createElement(Text, { wrap: 'truncate-end' }, ...rightParts) : undefined);
1131
+ { paddingLeft: 2 + indent, width: columns, justifyContent: rightParts.length > 0 ? leftParts.length > 0 ? 'space-between' : 'flex-end' : undefined }, leftParts.length > 0 ? createElement(Text, { wrap: 'truncate-end' }, ...leftParts) : undefined, rightParts.length > 0 ? createElement(Text, { wrap: 'truncate-end' }, ...rightParts) : undefined);
1133
1132
  };
1134
1133
  const row2Present = layout.row2.left.length > 0 || layout.row2.right.length > 0;
1135
- return createElement(Box, { flexDirection: 'column' }, renderRow(layout.row1, 's1'), row2Present ? renderRow(layout.row2, 's2', STATUS_ROW2_INDENT) : undefined);
1134
+ return createElement(Box, { flexDirection: 'column' }, renderRow(layout.row1, 's1'), row2Present ? renderRow(layout.row2, 's2', STATUS_ROW2_INDENT) : undefined, layout.row3 !== undefined ? renderRow(layout.row3, 's3', STATUS_ROW2_INDENT) : undefined);
1136
1135
  }
1137
1136
  /**
1138
1137
  * One fixed-height local feedback row. Errors remain visible while a slash
@@ -3691,9 +3690,8 @@ function DscodeUltraFocus({ width, animations }) {
3691
3690
  export function DscodeEffortBar({ row, current, select, back, onExit, animations = true }) {
3692
3691
  const ids = ['low', 'high', 'max', 'ultra'];
3693
3692
  const advertised = row.reasoning.efforts;
3694
- const defaultEffort = row.reasoning.defaultEffort;
3695
- const initial = current || defaultEffort;
3696
- const [cursor, setCursor] = useState(Math.max(0, ids.indexOf(initial)));
3693
+ const initial = resolveModelEffort(row, current);
3694
+ const [cursor, setCursor] = useState(Math.max(0, ids.indexOf(initial ?? '')));
3697
3695
  useStableInput((input, key) => {
3698
3696
  if (key.ctrl && input === 'c')
3699
3697
  return onExit();
@@ -3726,7 +3724,7 @@ export function DscodeEffortBar({ row, current, select, back, onExit, animations
3726
3724
  if (compact)
3727
3725
  return createElement(Box, { flexDirection: 'column', paddingX: 2 }, createElement(Text, { color: inkColor(palette.brandBright), wrap: 'truncate-end' }, truncateColumns(ids.map(id => id === selected ? '[' + id + ']' : id).join(' '), contentWidth)), createElement(Text, { color: inkColor(palette.dim), wrap: 'truncate-end' }, truncateColumns('←→ adjust · enter confirm · esc cancel', contentWidth)));
3728
3726
  const description = advertised.find(effort => effort.id === selected)?.description ?? '';
3729
- const currentLabel = current || defaultEffort || 'default';
3727
+ const currentLabel = initial || 'default';
3730
3728
  const footer = '←/→ adjust · Enter confirm · Esc cancel' + (hasOff ? ' · o off' : '');
3731
3729
  const trackNode = Math.floor((slot - 1) / 2);
3732
3730
  const pointerColumn = barIndent + cursor * slot + trackNode;
@@ -3754,7 +3752,7 @@ export function DscodeEffortPanel(props) {
3754
3752
  barIds.every(id => ids.includes(id)) && ids.every(id => id === 'off' || barIds.includes(id));
3755
3753
  return createElement(barCatalog ? DscodeEffortBar : NativeEffortPanel, props);
3756
3754
  }
3757
- function Input({ effortSurface, ultraPulse, active, frozen, frozenHint, busy, descriptors, skills, dispatch, steer, submitMode, cycleSubmitMode, interrupt, quit, openEmail, openLogin, openProvider, openOpenRouter, openModel, openEffort, openHelp, openMode, openPermission, openResume, openSearch, openPlugin, openUpdate, openSchedule, openJobs, openStatusline, openTheme, openLanguage, saveLanguage, openHistory, openQueue, openBtw, openAgents, openSubagent, openTodos, openUsage, openDelete, openDiff, openReviewPicker, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, inspectFiles, prepareFiles, readClipboardImage, cycleMode, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, updateQueued, historyFill, historyConsumed, animations, applyAnimations, applyRainbow, rainbowBurstId, waveTier, waveStyle, maxRows, anchorRowsBelow, tabTitle, onEditorRows, onMenuRows, sessionKey }) {
3755
+ function Input({ effortSurface, ultraPulse, active, frozen, frozenHint, busy, descriptors, skills, dispatch, steer, submitMode, cycleSubmitMode, interrupt, quit, openEmail, openLogin, openProvider, openOpenRouter, openModel, openEffort, openHelp, openPermission, openResume, openSearch, openPlugin, openUpdate, openSchedule, openJobs, openStatusline, openTheme, openLanguage, saveLanguage, openHistory, openQueue, openBtw, openAgents, openSubagent, openTodos, openUsage, openDelete, openDiff, openReviewPicker, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, inspectFiles, prepareFiles, readClipboardImage, cycleMode, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, updateQueued, historyFill, historyConsumed, animations, applyAnimations, applyRainbow, rainbowBurstId, waveTier, waveStyle, maxRows, anchorRowsBelow, tabTitle, onEditorRows, onMenuRows, sessionKey }) {
3758
3756
  const { stdout: inputStdout } = useStdout();
3759
3757
  const columns = inputStdout?.columns ?? 80;
3760
3758
  const inputTerminalRows = inputStdout?.rows ?? 30;
@@ -4676,11 +4674,7 @@ function Input({ effortSurface, ultraPulse, active, frozen, frozenHint, busy, de
4676
4674
  return;
4677
4675
  }
4678
4676
  if (text === '/mode' || text.startsWith('/mode ')) {
4679
- const mode = text.slice(5).trim();
4680
- if (mode === '')
4681
- openMode();
4682
- else
4683
- dispatch(text);
4677
+ notify(t('notice.modeLocked'), 'info');
4684
4678
  return;
4685
4679
  }
4686
4680
  if (text === '/resume cancel') {
@@ -5599,7 +5593,6 @@ export function App(props) {
5599
5593
  const [diffView, setDiffView] = useState(undefined);
5600
5594
  const [reviewPickerOpen, setReviewPickerOpen] = useState(false);
5601
5595
  const [helpOpen, setHelpOpen] = useState(false);
5602
- const [modeOpen, setModeOpen] = useState(false);
5603
5596
  const [permissionOpen, setPermissionOpen] = useState(false);
5604
5597
  const [resumeOpen, setResumeOpen] = useState(false);
5605
5598
  const [searchOpen, setSearchOpen] = useState(false);
@@ -5687,8 +5680,8 @@ export function App(props) {
5687
5680
  // panel keypress.
5688
5681
  const inputActive = deleteConfirmId !== undefined
5689
5682
  ? !approvalPending && !questionPending
5690
- : !emailOpen && !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !languageOpen && !historyOpen && !queueOpen && !agentsOpen && !subagentOpen && !todosOpen && !usageOpen && !verboseOpen && budgetPending === undefined && diffView === undefined && !reviewPickerOpen && !approvalPending && !questionPending;
5691
- const transcriptVisible = !btwOpen && !emailOpen && !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !languageOpen && !historyOpen && !queueOpen && !agentsOpen && !subagentOpen && !todosOpen && !usageOpen && !verboseOpen && budgetPending === undefined && diffView === undefined && !reviewPickerOpen && !approvalPending && !questionPending;
5683
+ : !emailOpen && !modelOpen && !helpOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !languageOpen && !historyOpen && !queueOpen && !agentsOpen && !subagentOpen && !todosOpen && !usageOpen && !verboseOpen && budgetPending === undefined && diffView === undefined && !reviewPickerOpen && !approvalPending && !questionPending;
5684
+ const transcriptVisible = !btwOpen && !emailOpen && !modelOpen && !helpOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !languageOpen && !historyOpen && !queueOpen && !agentsOpen && !subagentOpen && !todosOpen && !usageOpen && !verboseOpen && budgetPending === undefined && diffView === undefined && !reviewPickerOpen && !approvalPending && !questionPending;
5692
5685
  // Human questions outrank local inspectors. Close the lower modal instead
5693
5686
  // of leaving an approval/question visible but keyboard-locked behind it.
5694
5687
  useEffect(() => {
@@ -5700,7 +5693,6 @@ export function App(props) {
5700
5693
  setProviderAction(undefined);
5701
5694
  setEffortFor(undefined);
5702
5695
  setHelpOpen(false);
5703
- setModeOpen(false);
5704
5696
  setPermissionOpen(false);
5705
5697
  setResumeOpen(false);
5706
5698
  setPluginOpen(false);
@@ -5811,7 +5803,7 @@ export function App(props) {
5811
5803
  setMenuRows(current => (current === rows ? current : rows));
5812
5804
  }, []);
5813
5805
  // The status footer's exact row count, reported one-way by StatusLine (the
5814
- // second row renders only while it has content). The IME cursor anchor
5806
+ // extra rows render only while they have content). The IME cursor anchor
5815
5807
  // counts every row between the composer caret and Ink's parked cursor: the
5816
5808
  // status footer plus Ink's own below-frame row. The gutter rows sit ABOVE
5817
5809
  // the composer and never enter this distance.
@@ -5896,7 +5888,7 @@ export function App(props) {
5896
5888
  const auditedReasoningRows = liveAudit.allocation.reasoning;
5897
5889
  const auditedAnswerRows = liveAudit.allocation.answer;
5898
5890
  const inspectorVisible = verboseOpen && !approvalPending && !questionPending;
5899
- const modalVisible = budgetPending !== undefined || emailOpen || modelOpen || helpOpen && modeOpen || permissionOpen || resumeOpen || pluginOpen || updateOpen || scheduleOpen || jobsOpen || statuslineOpen || themeOpen || languageOpen || historyOpen || queueOpen || agentsOpen || subagentOpen || todosOpen || usageOpen || inspectorVisible || diffView !== undefined || reviewPickerOpen || approvalPending || questionPending;
5891
+ const modalVisible = budgetPending !== undefined || emailOpen || modelOpen || helpOpen || permissionOpen || resumeOpen || pluginOpen || updateOpen || scheduleOpen || jobsOpen || statuslineOpen || themeOpen || languageOpen || historyOpen || queueOpen || agentsOpen || subagentOpen || todosOpen || usageOpen || inspectorVisible || diffView !== undefined || reviewPickerOpen || approvalPending || questionPending;
5900
5892
  // The surface that currently owns the keyboard, named in the frozen band:
5901
5893
  // an empty composer under a panel must not advertise typing it cannot
5902
5894
  // accept — every key actually feeds the panel (which may or may not
@@ -5913,39 +5905,37 @@ export function App(props) {
5913
5905
  ? '/model'
5914
5906
  : helpOpen
5915
5907
  ? '/help'
5916
- : modeOpen
5917
- ? '/mode'
5918
- : permissionOpen
5919
- ? '/permission'
5920
- : resumeOpen
5921
- ? '/resume'
5922
- : pluginOpen
5923
- ? '/plugin'
5924
- : updateOpen
5925
- ? '/update'
5926
- : scheduleOpen
5927
- ? '/schedule'
5928
- : jobsOpen
5929
- ? '/jobs'
5930
- : statuslineOpen
5931
- ? '/statusline'
5932
- : themeOpen
5933
- ? '/theme'
5934
- : languageOpen
5935
- ? '/language'
5936
- : historyOpen
5937
- ? '/history'
5938
- : agentsOpen
5939
- ? '/agents'
5940
- : subagentOpen
5941
- ? '/subagent'
5942
- : todosOpen
5943
- ? '/todos'
5944
- : usageOpen
5945
- ? '/usage'
5946
- : inspectorVisible
5947
- ? 'history details'
5948
- : undefined;
5908
+ : permissionOpen
5909
+ ? '/permission'
5910
+ : resumeOpen
5911
+ ? '/resume'
5912
+ : pluginOpen
5913
+ ? '/plugin'
5914
+ : updateOpen
5915
+ ? '/update'
5916
+ : scheduleOpen
5917
+ ? '/schedule'
5918
+ : jobsOpen
5919
+ ? '/jobs'
5920
+ : statuslineOpen
5921
+ ? '/statusline'
5922
+ : themeOpen
5923
+ ? '/theme'
5924
+ : languageOpen
5925
+ ? '/language'
5926
+ : historyOpen
5927
+ ? '/history'
5928
+ : agentsOpen
5929
+ ? '/agents'
5930
+ : subagentOpen
5931
+ ? '/subagent'
5932
+ : todosOpen
5933
+ ? '/todos'
5934
+ : usageOpen
5935
+ ? '/usage'
5936
+ : inspectorVisible
5937
+ ? 'history details'
5938
+ : undefined;
5949
5939
  const frozenHint = keyboardOwner === undefined
5950
5940
  ? undefined
5951
5941
  : `keys go to ${keyboardOwner} · esc ${approvalPending ? 'rejects' : questionPending ? 'cancels' : 'closes'}`;
@@ -6018,7 +6008,9 @@ export function App(props) {
6018
6008
  try {
6019
6009
  const label = props.selectModel(row, effortId);
6020
6010
  setModelLabel(label);
6021
- setEffortLabel(effortId);
6011
+ // Keep the selected model's display default separate from the previous
6012
+ // request's effort. An empty label explicitly means no advertised default.
6013
+ setEffortLabel(resolveModelEffort(row, effortId) ?? '');
6022
6014
  setUltraPulse(effortId === 'ultra' && animations ? Date.now() : 0);
6023
6015
  const selected = `${label}${effortId === undefined || effortId === '' ? '' : `@${effortId}`}`;
6024
6016
  if (sessionHasImages && row.inputModalities !== undefined && !row.inputModalities.includes('image')) {
@@ -6502,18 +6494,6 @@ export function App(props) {
6502
6494
  entries: view.entries,
6503
6495
  onClose: closeInspector,
6504
6496
  })
6505
- : undefined, modeOpen && !approvalPending && !questionPending
6506
- ? createElement(ModePanel, {
6507
- current: props.mode,
6508
- load: props.loadPresets,
6509
- select: (id) => {
6510
- void props.switchMode(id).then(label => {
6511
- notify(t('notice.modeChangedSimple', { value: label }));
6512
- setModeOpen(false);
6513
- }, (reason) => notify(t('notice.modeSwitchFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error'));
6514
- },
6515
- close: () => setModeOpen(false),
6516
- })
6517
6497
  : undefined, permissionOpen && !approvalPending && !questionPending
6518
6498
  ? createElement(PermissionPanel, {
6519
6499
  current: props.permission,
@@ -6771,7 +6751,6 @@ export function App(props) {
6771
6751
  openHelp: () => {
6772
6752
  setHelpOpen(true);
6773
6753
  },
6774
- openMode: () => setModeOpen(true),
6775
6754
  openPermission: () => setPermissionOpen(true),
6776
6755
  openResume: () => { setResumeDelete({ mode: false }); setResumeOpen(true); },
6777
6756
  openSearch: (query) => {
@@ -6878,7 +6857,7 @@ export function App(props) {
6878
6857
  facts: {
6879
6858
  fullSessionId: props.sessionKey,
6880
6859
  model: modelLabel,
6881
- effort: effortLabel,
6860
+ effort: effortLabel ?? (modelLabel === view.model ? view.stats.reasoningEffort : undefined),
6882
6861
  mode: props.mode,
6883
6862
  cwd: props.cwd,
6884
6863
  branch: props.branch,
@@ -0,0 +1,18 @@
1
+ import { resolvePreset } from '../presets.mjs';
2
+ export const DSCODE_PRESET = 'dscode';
3
+ export function requireDscodePreset(id) {
4
+ if (id !== undefined && id !== DSCODE_PRESET)
5
+ throw new Error('DSCODE is locked to the dscode preset; use /new to start a DSCODE session.');
6
+ return DSCODE_PRESET;
7
+ }
8
+ /** Mount before recording a migration; failed composition must leave history intact. */
9
+ export async function mountDscodePreset(service, ctx, session) {
10
+ const mounted = await service.mount(ctx, DSCODE_PRESET);
11
+ if (mounted.id !== DSCODE_PRESET)
12
+ throw new Error('The dscode preset did not resolve to DSCODE.');
13
+ if (resolvePreset(session) !== DSCODE_PRESET) {
14
+ const writable = session;
15
+ writable.append('agent-preset/selected', { agentPreset: DSCODE_PRESET });
16
+ }
17
+ return mounted;
18
+ }
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * @module dsh-code/dscode/telemetry
6
6
  */
7
- import { createElement } from 'react';
7
+ import { createElement, useEffect, useState } from 'react';
8
8
  import { Text } from 'ink';
9
9
  import { getPalette, getTheme, inkColor } from '../theme.mjs';
10
10
  /** Request-rate tiers: yellow under 75 tps, green to 150, blue to 250, purple above. */
@@ -60,13 +60,15 @@ export function dscodeTelemetryParts(value) {
60
60
  result.push({ text: ' · ', tone: null });
61
61
  // A rate figure reserves its columns, so the padding stays outside the tinted
62
62
  // run and only the reading carries the tier.
63
- const rate = /^(\s*)(~?(?:\d+(?:\.\d+)?|--)) tps( \S+)?$/.exec(part);
63
+ const rate = /^(◌ )?(\s*)(\d+(?:\.\d+)?|--) tps( \S+)?$/.exec(part);
64
64
  if (rate) {
65
- const display = rate[2];
66
- result.push({ text: rate[1], tone: null }, { text: display + ' tps', tone: dscodeTpsTone(Number(display.startsWith('~') ? display.slice(1) : display)) });
65
+ if (rate[1])
66
+ result.push({ text: rate[1], tone: null, activity: true });
67
+ const display = rate[3];
68
+ result.push({ text: rate[2], tone: null }, { text: display + ' tps', tone: dscodeTpsTone(Number(display)) });
67
69
  // The average's trailing qualifier rides along untinted.
68
- if (rate[3] !== undefined)
69
- result.push({ text: rate[3], tone: null });
70
+ if (rate[4] !== undefined)
71
+ result.push({ text: rate[4], tone: null });
70
72
  continue;
71
73
  }
72
74
  // Only a recognised cache label on the very last figure is tinted; the label
@@ -83,7 +85,21 @@ export function dscodeTelemetryParts(value) {
83
85
  }
84
86
  return result;
85
87
  }
86
- /** The telemetry run as Ink text nodes, one per tinted segment. */
87
- export function dscodeTelemetryNodes(value, key) {
88
- return dscodeTelemetryParts(value).map((part, index) => createElement(Text, { key: key + 'p' + index, color: dscodeTpsInkColor(part.tone) }, part.text));
88
+ /** Only this leaf ticks; neither the numeric reading nor ledger polling animates. */
89
+ function TpsActivity({ animated }) {
90
+ const [frame, setFrame] = useState(0);
91
+ useEffect(() => {
92
+ if (!animated)
93
+ return;
94
+ const timer = setInterval(() => setFrame(value => value + 1), 120);
95
+ return () => clearInterval(timer);
96
+ }, [animated]);
97
+ const frames = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
98
+ return createElement(Text, { color: inkColor(getPalette().brandMid) }, (animated ? frames[frame % frames.length] : '◌') + ' ');
99
+ }
100
+ /** The telemetry run as Ink nodes; only the request activity slot may move. */
101
+ export function dscodeTelemetryNodes(value, key, animated = false) {
102
+ return dscodeTelemetryParts(value).map((part, index) => part.activity
103
+ ? createElement(TpsActivity, { key: key + 'p' + index, animated })
104
+ : createElement(Text, { key: key + 'p' + index, color: dscodeTpsInkColor(part.tone) }, part.text));
89
105
  }
@@ -51,7 +51,8 @@ import { beginProviderAuthorization, cancelProviderAuthorization, loadProviderAu
51
51
  import { selectForkSeed } from './fork.mjs';
52
52
  import { buildReviewPrompt, listReviewBranches, listReviewCommits, loadCommitDiff, loadGitDiff, mergeBaseWith, } from './git-workflow.mjs';
53
53
  import { SessionSwitchQueue } from './session-switch.mjs';
54
- import { agentPresetsFrom, normalizePresetId, resolvePreset, selectPreset } from './presets.mjs';
54
+ import { agentPresetsFrom } from './presets.mjs';
55
+ import { DSCODE_PRESET, requireDscodePreset, mountDscodePreset } from './dscode/preset.mjs';
55
56
  import { applyPendingPermission, effectivePermission, listPermissionRows, permissionPresetsFrom, selectPermission, } from './permissions.mjs';
56
57
  import { listPluginRows } from './plugin-inventory.mjs';
57
58
  import { applyLauncherUpdate, probeLauncherUpdate } from './update.mjs';
@@ -472,6 +473,8 @@ async function run(ctx, startup, io) {
472
473
  // Early process shutdown can dispose the tree while settlement is pending.
473
474
  if (agents === undefined || defaultModel === undefined || sessions === undefined)
474
475
  return;
476
+ if (startup.kind === 'fresh' || startup.kind === 'named')
477
+ requireDscodePreset(startup.mode);
475
478
  const cwd = process.cwd();
476
479
  // Live deployment default (web selectModel parity): read on every use, not
477
480
  // snapshotted at launch, so a /model pick this process saves becomes the
@@ -494,20 +497,12 @@ async function run(ctx, startup, io) {
494
497
  const selectionState = pendingSelection === undefined
495
498
  ? {}
496
499
  : { picked: pendingSelection };
497
- let mode = next.resume ? next.mode : next.mode ?? pendingMode;
498
- // An explicit `--mode` or the settings-layer service default may still name
499
- // an id an upstream rename retired (code → ptc); normalize both.
500
- if (!next.resume)
501
- mode = (await presets.resolve(normalizePresetId(mode ?? presets.defaultId))).id;
500
+ const mode = requireDscodePreset(next.mode);
502
501
  // 0.1.5 AgentSetup passes the composed agent as its second argument (the
503
502
  // former `ctx.agent` accessor is gone); the preset mount still needs the
504
503
  // agent-scoped context.
505
504
  const setup = async (agentCtx, agent) => {
506
- const sessionPreset = next.resume
507
- ? resolvePreset(agent.session)
508
- : mode;
509
- const mounted = await presets.mount(agentCtx, sessionPreset);
510
- mode = mounted.id;
505
+ await mountDscodePreset(presets, agentCtx, agent.session);
511
506
  const selection = {
512
507
  get current() {
513
508
  return resolveEffectiveSelection(selectionState.picked, agent.session.requestHeader()?.config, currentDefaults());
@@ -578,7 +573,7 @@ async function run(ctx, startup, io) {
578
573
  session,
579
574
  store: createTranscriptStore(seedEvents),
580
575
  mentions: createMentions(ctx, handle.agent, session.header.cwd ?? nextCwd),
581
- mode: mode ?? 'standard',
576
+ mode,
582
577
  selection: selectionState,
583
578
  resumed: next.resume,
584
579
  ...(folderWarning === undefined ? {} : { folderWarning }),
@@ -665,10 +660,6 @@ async function run(ctx, startup, io) {
665
660
  let mentions = createMentions(ctx, undefined, cwd);
666
661
  /** Explicit model pick made before any session exists (a bare launch). */
667
662
  let pendingSelection;
668
- /** Agent preset selected before the first session exists. */
669
- let pendingMode;
670
- /** Ordered pre-session preset resolutions; first composition awaits them. */
671
- let pendingModeWork = Promise.resolve();
672
663
  /** Permission preset selected before the first session exists. */
673
664
  let pendingPermission;
674
665
  /**
@@ -701,7 +692,7 @@ async function run(ctx, startup, io) {
701
692
  }
702
693
  preSessionPlanKnown = false;
703
694
  void presets.compositionInventory().then(inventory => {
704
- const id = pendingMode ?? normalizePresetId(presets.defaultId);
695
+ const id = DSCODE_PRESET;
705
696
  preSessionPlanAvailable = inventory.some(composition => composition.id === id
706
697
  && composition.rows.some(row => row.moduleName === '@deepseek-ai/dsh-plan-mode' && row.enabled !== false));
707
698
  preSessionPlanKnown = true;
@@ -1276,9 +1267,6 @@ async function run(ctx, startup, io) {
1276
1267
  creating = true;
1277
1268
  void compose(async () => {
1278
1269
  try {
1279
- // A direct `/mode <preset>` resolves asynchronously. Preserve submit
1280
- // order so the first composition cannot race ahead with the old mode.
1281
- await pendingModeWork;
1282
1270
  // Another composition (e.g. a /resume activated while this creation
1283
1271
  // waited its turn) may have published a session already: deliver the
1284
1272
  // queued lines there instead of minting a competing fresh session
@@ -1308,7 +1296,6 @@ async function run(ctx, startup, io) {
1308
1296
  subagents.reset();
1309
1297
  for (const event of next.catalogSeed)
1310
1298
  subagents.apply(event.data.childId, event);
1311
- pendingMode = undefined;
1312
1299
  pendingPermission = undefined;
1313
1300
  commands.setAgent(agent);
1314
1301
  skills.setAgent(agent);
@@ -1369,8 +1356,8 @@ async function run(ctx, startup, io) {
1369
1356
  const line = submissionPayload(text);
1370
1357
  if (line.trim() === '' && images.length === 0)
1371
1358
  return;
1372
- if (images.length === 0 && line.startsWith('/mode ')) {
1373
- void switchModeAction(line.slice(6).trim()).then(selected => bridge.notify(`mode → ${selected}`), error => bridge.notify(`mode switch failed: ${error instanceof Error ? error.message : String(error)}`, 'error'));
1359
+ if (images.length === 0 && (line === '/mode' || line.startsWith('/mode '))) {
1360
+ bridge.notify('DSCODE mode is fixed to dscode.', 'info');
1374
1361
  return;
1375
1362
  }
1376
1363
  if (images.length === 0 && line.startsWith('/permission ')) {
@@ -1801,50 +1788,6 @@ async function run(ctx, startup, io) {
1801
1788
  turns: turnUsages(events, deriveTurnTokenUsage, turnCostsFor(current.id, events)),
1802
1789
  });
1803
1790
  };
1804
- const switchModeAction = async (id) => {
1805
- if (id === '')
1806
- throw new Error('usage: /mode <preset>');
1807
- const currentAgent = agent;
1808
- if (currentAgent === undefined) {
1809
- const choice = pendingModeWork.then(async () => {
1810
- const preset = await selectPreset(presets, undefined, id);
1811
- // A resume may have won while this roster read was in flight; never
1812
- // leak the old pending choice into a later /new session.
1813
- if (agent === undefined) {
1814
- pendingMode = preset.id;
1815
- renderCurrent();
1816
- }
1817
- return preset.id;
1818
- });
1819
- pendingModeWork = choice.then(() => { }, () => { });
1820
- return choice;
1821
- }
1822
- // Serialize the recomposition with session activations: a /mode that
1823
- // interleaves a switch must not rebind the shared command/skill
1824
- // registries while the switch is composing the next agent.
1825
- const currentActive = active;
1826
- const atEpoch = epoch;
1827
- let selected;
1828
- await compose(async () => {
1829
- const preset = await selectPreset(presets, currentAgent, id);
1830
- // A switch/quit landed while the recomposition ran: applying here
1831
- // would write the old choice into the new session's state and rebind
1832
- // the registries back to a disposed agent. The preset-selection log
1833
- // entry rode the old agent's session; only the local application is
1834
- // dropped.
1835
- if (epoch !== atEpoch || agent !== currentAgent || active !== currentActive) {
1836
- throw new Error('session changed while switching mode — nothing applied; retry in the active session');
1837
- }
1838
- if (active === undefined)
1839
- throw new Error('active Agent has no session state');
1840
- active.mode = preset.id;
1841
- commands.setAgent(currentAgent);
1842
- skills.setAgent(currentAgent);
1843
- selected = preset.id;
1844
- renderCurrent();
1845
- });
1846
- return selected;
1847
- };
1848
1791
  const activate = (nextTarget) => {
1849
1792
  if (quitting)
1850
1793
  return Promise.resolve();
@@ -1885,12 +1828,11 @@ async function run(ctx, startup, io) {
1885
1828
  renderCurrent();
1886
1829
  // Only a successful handoff may clear the transient per-session
1887
1830
  // surfaces: a rolled-back switch keeps the previous session's
1888
- // subagent feed plus the user's pre-session /mode and permission
1831
+ // subagent feed plus the user's pre-session model and permission
1889
1832
  // picks (the bare-launch promise: explicit choices survive until
1890
1833
  // composition takes them). The in-flight cycle intent belonged to
1891
1834
  // the previous session's presses; the new session's committed fold
1892
1835
  // decides from here.
1893
- pendingMode = undefined;
1894
1836
  pendingPermission = undefined;
1895
1837
  pendingPlan = false;
1896
1838
  planIntent = undefined;
@@ -2007,6 +1949,13 @@ async function run(ctx, startup, io) {
2007
1949
  }, (error) => bridge.notify(`resume failed: ${error instanceof Error ? error.message : String(error)}`, 'error'));
2008
1950
  };
2009
1951
  const createSession = (mode) => {
1952
+ try {
1953
+ requireDscodePreset(mode);
1954
+ }
1955
+ catch (error) {
1956
+ bridge.notify(error instanceof Error ? error.message : String(error), 'error');
1957
+ return;
1958
+ }
2010
1959
  // /new before any input is the first-session creation itself, not a switch.
2011
1960
  if (session === undefined) {
2012
1961
  ensureSession(mode);
@@ -2199,12 +2148,9 @@ async function run(ctx, startup, io) {
2199
2148
  const sessionCwd = session?.header.cwd ?? cwd;
2200
2149
  const currentView = store.getView();
2201
2150
  const defaults = currentDefaults();
2202
- const model = currentView.model !== ''
2203
- ? currentView.model
2204
- : pendingSelection !== undefined
2205
- ? `${pendingSelection.provider}/${pendingSelection.model}`
2206
- : `${defaults.provider}/${defaults.model}`;
2207
- const effort = resolveEffectiveSelection(active?.selection.picked ?? pendingSelection, session?.requestHeader()?.config, defaults).reasoningEffort;
2151
+ const selection = resolveEffectiveSelection(active?.selection.picked ?? pendingSelection, session?.requestHeader()?.config, defaults);
2152
+ const model = `${selection.provider}/${selection.model}`;
2153
+ const effort = selection.reasoningEffort;
2208
2154
  const permission = permissionPresets === undefined
2209
2155
  ? currentView.permission
2210
2156
  : effectivePermission(permissionPresets, session, pendingPermission);
@@ -2227,7 +2173,7 @@ async function run(ctx, startup, io) {
2227
2173
  branch: gitBranch(sessionCwd),
2228
2174
  sessionId: session === undefined ? '' : session.id.slice(-8),
2229
2175
  resumed: active?.resumed ?? false,
2230
- mode: active?.mode ?? pendingMode ?? normalizePresetId(presets.defaultId),
2176
+ mode: active?.mode ?? DSCODE_PRESET,
2231
2177
  permission,
2232
2178
  /** Pre-session plan choice for the status badge until a session composes. */
2233
2179
  pendingPlan: session === undefined && pendingPlan,
@@ -2277,8 +2223,6 @@ async function run(ctx, startup, io) {
2277
2223
  listReviewBranches: (signal) => listReviewBranches(session?.header.cwd ?? cwd, signal),
2278
2224
  listReviewCommits: (signal) => listReviewCommits(session?.header.cwd ?? cwd, signal),
2279
2225
  reviewChanges,
2280
- loadPresets: () => presets.list(),
2281
- switchMode: switchModeAction,
2282
2226
  loadPermissions: () => permissionPresets === undefined
2283
2227
  ? Promise.reject(new Error('permission presets are not mounted in this composition'))
2284
2228
  : Promise.resolve(listPermissionRows(permissionPresets)),