@deepseek-harness-tui/dsh-tui 0.7.2 → 0.7.3

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 (65) hide show
  1. package/README.md +1 -1
  2. package/bin/dsh-tui.js +5 -0
  3. package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
  4. package/lib/types/components/GoalTodoPanel.js +6 -1
  5. package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
  6. package/lib/types/components/HistorySearchDialog.js +5 -1
  7. package/lib/types/components/MessageList.d.ts.map +1 -1
  8. package/lib/types/components/MessageList.js +5 -1
  9. package/lib/types/components/PromptInput.d.ts.map +1 -1
  10. package/lib/types/components/PromptInput.js +52 -74
  11. package/lib/types/components/SearchBox.d.ts +4 -0
  12. package/lib/types/components/SearchBox.d.ts.map +1 -1
  13. package/lib/types/components/SearchBox.js +101 -2
  14. package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts +4 -1
  15. package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts.map +1 -1
  16. package/lib/types/components/Spinner/SpinnerAnimationRow.js +10 -5
  17. package/lib/types/components/WorkingSpinner.d.ts +3 -1
  18. package/lib/types/components/WorkingSpinner.d.ts.map +1 -1
  19. package/lib/types/components/WorkingSpinner.js +2 -2
  20. package/lib/types/components/design-system/ThemedText.d.ts +7 -1
  21. package/lib/types/components/design-system/ThemedText.d.ts.map +1 -1
  22. package/lib/types/components/design-system/ThemedText.js +2 -2
  23. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  24. package/lib/types/components/questions/AskUserQuestionPanel.js +52 -8
  25. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  26. package/lib/types/components/questions/PlanReviewPanel.js +12 -3
  27. package/lib/types/dsh-adapter/channel.d.ts +10 -0
  28. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  29. package/lib/types/dsh-adapter/channel.js +45 -9
  30. package/lib/types/dsh-adapter/packaged-presets.d.ts.map +1 -1
  31. package/lib/types/dsh-adapter/packaged-presets.js +5 -2
  32. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  33. package/lib/types/dsh-adapter/plugin.js +7 -3
  34. package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
  35. package/lib/types/dsh-adapter/providerWizard.js +3 -1
  36. package/lib/types/i18n.d.ts +32 -8
  37. package/lib/types/i18n.d.ts.map +1 -1
  38. package/lib/types/i18n.js +9 -3
  39. package/lib/types/ink/components/Text.d.ts +9 -1
  40. package/lib/types/ink/components/Text.d.ts.map +1 -1
  41. package/lib/types/ink/components/Text.js +7 -6
  42. package/lib/types/ink/ink.d.ts.map +1 -1
  43. package/lib/types/ink/ink.js +4 -2
  44. package/lib/types/ink/log-update.d.ts.map +1 -1
  45. package/lib/types/ink/log-update.js +76 -9
  46. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  47. package/lib/types/ink/parse-keypress.js +4 -1
  48. package/lib/types/ink/terminal.d.ts +40 -4
  49. package/lib/types/ink/terminal.d.ts.map +1 -1
  50. package/lib/types/ink/terminal.js +57 -5
  51. package/lib/types/ink/termio/tokenize.d.ts +6 -0
  52. package/lib/types/ink/termio/tokenize.d.ts.map +1 -1
  53. package/lib/types/ink/termio/tokenize.js +18 -4
  54. package/lib/types/screens/Chat.d.ts.map +1 -1
  55. package/lib/types/screens/Chat.js +76 -16
  56. package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
  57. package/lib/types/screens/SessionBrowser.js +30 -15
  58. package/lib/types/sessions/view.d.ts.map +1 -1
  59. package/lib/types/sessions/view.js +13 -1
  60. package/lib/types/update.d.ts +21 -7
  61. package/lib/types/update.d.ts.map +1 -1
  62. package/lib/types/update.js +66 -16
  63. package/lib/types/utils/paths.d.ts.map +1 -1
  64. package/lib/types/utils/paths.js +4 -1
  65. package/package.json +33 -9
@@ -243,6 +243,14 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
243
243
  }, []);
244
244
  /** Startup context panel: expanded by header click or Ctrl+T. */
245
245
  const [loadedContextOpen, setLoadedContextOpen] = React.useState(false);
246
+ /**
247
+ * Whether the startup context panel is on screen. Derived once and read by
248
+ * BOTH the Ctrl+T handler and the render below: the key's meaning depends on
249
+ * the panel being visible, so a second copy of this condition is a place for
250
+ * the two to disagree — which is how the key came to advertise one thing and
251
+ * do another in the first place.
252
+ */
253
+ const loadedContextVisible = channel.rows.length === 0 && channel.loadedContext !== undefined;
246
254
  /** `/` transcript search (less-style incsearch, ported from CC's REPL). */
247
255
  const [searchOpen, setSearchOpen] = React.useState(false);
248
256
  const [searchQuery, setSearchQuery] = React.useState('');
@@ -306,10 +314,17 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
306
314
  // Spinner timing refs, fed from channel state each render (the spinner
307
315
  // only mounts while working, so values are stable for the mount).
308
316
  const responseLengthRef = React.useRef(0);
317
+ const uploadTokensRef = React.useRef(0);
309
318
  const loadingStartTimeRef = React.useRef(0);
310
319
  const totalPausedMsRef = React.useRef(0);
311
320
  const pauseStartTimeRef = React.useRef(null);
312
321
  responseLengthRef.current = channel.responseChars;
322
+ // Most recent request's real upload (input + cache read/write occupy the
323
+ // wire exactly like the context window); 0 until the first usage event.
324
+ const lastUploadTokens = channel.lastUsage === undefined
325
+ ? 0
326
+ : channel.lastUsage.input + channel.lastUsage.cacheRead + channel.lastUsage.cacheWrite;
327
+ uploadTokensRef.current = lastUploadTokens;
313
328
  loadingStartTimeRef.current = channel.turnStart;
314
329
  const thinkingStatus = useThinkingStatus(channel.spinnerMode === 'thinking');
315
330
  // Terminal tab title (ported from CC's AnimatedTerminalTitle): the session
@@ -814,14 +829,28 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
814
829
  });
815
830
  return true;
816
831
  case 'login': {
817
- const key = process.env.DEEPSEEK_API_KEY;
818
- const lines = [
819
- `${t('login-api-key', { key: key ? key.slice(0, 6) + '…' + key.slice(-4) : t('login-key-missing') })}`,
820
- `${t('login-base-url', { url: process.env.DEEPSEEK_BASE_URL ?? t('login-official-endpoint') })}`,
821
- t('login-source-hint'),
822
- ];
823
832
  setHelpOpen(false);
824
- channel.pushLocal('/login', lines);
833
+ void channel.describeCredential('DEEPSEEK_API_KEY')
834
+ .catch(() => undefined)
835
+ .then(status => {
836
+ const keyStatus = status === undefined
837
+ ? t('login-credentials-unavailable')
838
+ : status.configured
839
+ ? t('login-key-configured', { ref: 'DEEPSEEK_API_KEY' })
840
+ : t('login-key-missing');
841
+ channel.pushLocal('/login', [
842
+ t('login-api-key', { status: keyStatus }),
843
+ ...(status === undefined
844
+ ? []
845
+ : [
846
+ t('login-credential-source', { source: status.source ?? t('login-source-none') }),
847
+ t('login-credential-storage', {
848
+ mode: t(status.writable ? 'login-storage-writable' : 'login-storage-read-only'),
849
+ }),
850
+ ]),
851
+ t('login-base-url', { url: process.env.DEEPSEEK_BASE_URL ?? t('login-official-endpoint') }),
852
+ ]);
853
+ });
825
854
  return true;
826
855
  }
827
856
  case 'logout':
@@ -1538,10 +1567,22 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
1538
1567
  }
1539
1568
  }
1540
1569
  else if (key.leftArrow) {
1541
- setHistoryCursor(cursor => Math.max(0, cursor - 1));
1570
+ // Step by code point, not UTF-16 unit: an emoji is two units, and
1571
+ // a mid-pair caret offset would split it in the SearchBox render.
1572
+ setHistoryCursor(cursor => {
1573
+ if (cursor <= 0)
1574
+ return 0;
1575
+ const ch = [...historyQuery.slice(0, cursor)].pop();
1576
+ return cursor - ch.length;
1577
+ });
1542
1578
  }
1543
1579
  else if (key.rightArrow) {
1544
- setHistoryCursor(cursor => Math.min(historyQuery.length, cursor + 1));
1580
+ setHistoryCursor(cursor => {
1581
+ if (cursor >= historyQuery.length)
1582
+ return historyQuery.length;
1583
+ const ch = [...historyQuery.slice(cursor)][0];
1584
+ return cursor + ch.length;
1585
+ });
1545
1586
  }
1546
1587
  else if (key.home) {
1547
1588
  setHistoryCursor(0);
@@ -1589,11 +1630,25 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
1589
1630
  return;
1590
1631
  }
1591
1632
  if (isMod(key) && input === 't') {
1592
- // Ctrl+T opens the trajectory scene (issue #80 evolution). It used to
1593
- // toggle the startup loaded-context panel — which only ever renders
1594
- // before the first message (see the render below), so the binding was
1595
- // dead for the whole rest of a session. The panel keeps its header
1596
- // click-to-toggle; the key now has a meaning that always applies.
1633
+ // Ctrl+T means "expand what is in front of you", and the two things it
1634
+ // can expand never share the screen.
1635
+ //
1636
+ // The startup loaded-context panel renders only while the transcript is
1637
+ // empty (see the render below) — and that is exactly the window where
1638
+ // the trajectory has nothing in it yet, since the trajectory is folded
1639
+ // from the session's own events. So the panel wins while it is up, and
1640
+ // the scene takes over for the rest of the session.
1641
+ //
1642
+ // Claiming the key outright for the scene was wrong even though the
1643
+ // panel binding looked dead: the panel prints its own `(Ctrl+T 展开)`
1644
+ // hint, so the key still had a visible promise attached to it, and the
1645
+ // scene it opened instead was necessarily empty at that moment. That
1646
+ // left the panel reachable only by clicking its header, which is not a
1647
+ // keyboard path at all.
1648
+ if (loadedContextVisible) {
1649
+ setLoadedContextOpen(previous => !previous);
1650
+ return;
1651
+ }
1597
1652
  openScene();
1598
1653
  return;
1599
1654
  }
@@ -1732,11 +1787,16 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
1732
1787
  seekRow(lastUser.id);
1733
1788
  else
1734
1789
  handle?.scrollToBottom();
1735
- } })), _jsxs(ScrollBox, { ref: setHandle, flexDirection: "column", flexGrow: 1, flexShrink: 1, stickyScroll: true, children: [_jsx(LogoHeader, { model: channel.model, effort: channel.reasoningEffort, cwd: channel.displayCwd }), channel.rows.length === 0 && channel.loadedContext !== undefined && (_jsx(LoadedContextPanel, { context: channel.loadedContext, open: loadedContextOpen, onToggle: () => { setLoadedContextOpen(previous => !previous); } })), _jsx(MessageList, { rows: channel.rows, failureHintRowId: failureHintRowId, failureHint: t('traj-hint-failure', { key: `${modLabel}t` }), expanded: expanded, expandedRows: expandedRows, selectedId: selectionActive ? selectedId : null, onToggleRow: toggleRowExpanded, model: channel.model, showAll: showAllMessages, thinkingVisible: thinkingVisible, onToggleAll: () => { setShowAllMessages(previous => !previous); }, onLoadOlder: () => channel.loadOlder(), registerRowRef: registerRowRef, scrollHandle: handle, forceMountRowId: forceMountRowId, newSinceRowId: isSticky ? null : lastSeenRowIdRef.current, onUnseenCount: setUnseenCount })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [showPill && (_jsx(NewMessagesPill, { count: unseenCount, onClick: () => handle?.scrollToBottom() })), channel.working &&
1790
+ } })), _jsxs(ScrollBox, { ref: setHandle, flexDirection: "column", flexGrow: 1, flexShrink: 1, stickyScroll: true, children: [_jsx(LogoHeader, { model: channel.model, effort: channel.reasoningEffort, cwd: channel.displayCwd }), loadedContextVisible && (_jsx(LoadedContextPanel, { context: channel.loadedContext, open: loadedContextOpen, onToggle: () => { setLoadedContextOpen(previous => !previous); } })), _jsx(MessageList, { rows: channel.rows, failureHintRowId: failureHintRowId, failureHint: t('traj-hint-failure', { key: `${modLabel}t` }), expanded: expanded, expandedRows: expandedRows, selectedId: selectionActive ? selectedId : null, onToggleRow: toggleRowExpanded, model: channel.model, showAll: showAllMessages, thinkingVisible: thinkingVisible, onToggleAll: () => { setShowAllMessages(previous => !previous); }, onLoadOlder: () => channel.loadOlder(), registerRowRef: registerRowRef, scrollHandle: handle, forceMountRowId: forceMountRowId, newSinceRowId: isSticky ? null : lastSeenRowIdRef.current, onUnseenCount: setUnseenCount })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [showPill && (_jsx(NewMessagesPill, { count: unseenCount, onClick: () => handle?.scrollToBottom() })), channel.working &&
1736
1791
  (channel.activityEnabled &&
1737
1792
  channel.workingActivity !== undefined &&
1738
1793
  channel.workingActivity.line !== '' &&
1739
- channel.workingActivity.phase !== 'idle' ? (_jsx(Box, { marginTop: 1, children: _jsx(ActivityLine, { activity: channel.workingActivity, activityFrames: channel.activityFrames, warnPct: activityWarnPct, warnDanger: activityWarnPct !== undefined && activityWarnPct >= 95, suffix: ` · ↓ ${channel.responseChars} tokens` }) })) : (_jsx(WorkingSpinner, { mode: channel.spinnerMode, hasActiveTools: channel.activeToolCount > 0, responseLengthRef: responseLengthRef, loadingStartTimeRef: loadingStartTimeRef, totalPausedMsRef: totalPausedMsRef, pauseStartTimeRef: pauseStartTimeRef, thinkingStatus: thinkingStatus }))), _jsx(GoalTodoPanel, { channel: channel }), approvalSnapshot !== null ? (_jsx(ApprovalPanel, { approval: approvalSnapshot, onDecide: outcome => approvals.decide(outcome) }, approvalSnapshot.key)) : btw !== null ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(BtwPanel, { question: btw.question, answer: btw.answer, error: btw.error, streaming: !btw.done, onClose: closeBtw, onCopy: () => {
1794
+ channel.workingActivity.phase !== 'idle' ? (_jsx(Box, { marginTop: 1, children: _jsx(ActivityLine, { activity: channel.workingActivity, activityFrames: channel.activityFrames, warnPct: activityWarnPct, warnDanger: activityWarnPct !== undefined && activityWarnPct >= 95,
1795
+ // Upload = real tokens of the last request; download =
1796
+ // the animated chars/4 estimate, matching the classic
1797
+ // spinner's counter (the suffix used raw chars before,
1798
+ // inflating the reading next to a real upload number).
1799
+ suffix: `${lastUploadTokens > 0 ? ` · ↑ ${formatTokens(lastUploadTokens)}` : ''} · ↓ ${formatTokens(Math.round(channel.responseChars / 4))} tokens` }) })) : (_jsx(WorkingSpinner, { mode: channel.spinnerMode, hasActiveTools: channel.activeToolCount > 0, responseLengthRef: responseLengthRef, uploadTokensRef: uploadTokensRef, loadingStartTimeRef: loadingStartTimeRef, totalPausedMsRef: totalPausedMsRef, pauseStartTimeRef: pauseStartTimeRef, thinkingStatus: thinkingStatus }))), _jsx(GoalTodoPanel, { channel: channel }), approvalSnapshot !== null ? (_jsx(ApprovalPanel, { approval: approvalSnapshot, onDecide: outcome => approvals.decide(outcome) }, approvalSnapshot.key)) : btw !== null ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(BtwPanel, { question: btw.question, answer: btw.answer, error: btw.error, streaming: !btw.done, onClose: closeBtw, onCopy: () => {
1740
1800
  void setClipboard(btw.answer ?? '').then(raw => { if (raw)
1741
1801
  process.stdout.write(raw); });
1742
1802
  channel.notify(t('copied-chars', { n: (btw.answer ?? '').length }), { timeoutMs: 1500 });
@@ -1 +1 @@
1
- {"version":3,"file":"SessionBrowser.d.ts","sourceRoot":"","sources":["../../../src/screens/SessionBrowser.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAsBzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAoDxD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,IAAI,EACJ,WAAW,EACX,OAAO,GACR,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;IAC9C,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,KAAK,CAAC,SAAS,CAydlB"}
1
+ {"version":3,"file":"SessionBrowser.d.ts","sourceRoot":"","sources":["../../../src/screens/SessionBrowser.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAsBzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAoDxD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,IAAI,EACJ,WAAW,EACX,OAAO,GACR,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;IAC9C,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,KAAK,CAAC,SAAS,CAselB"}
@@ -112,6 +112,10 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
112
112
  const [previewOpen, setPreviewOpen] = React.useState(false);
113
113
  const [preview, setPreview] = React.useState([]);
114
114
  const [previewLoading, setPreviewLoading] = React.useState(false);
115
+ // React batches every parsed key from one stdin chunk. Lock actions in a
116
+ // ref so a repeated Enter cannot start the same async operation twice
117
+ // before the mode change is rendered.
118
+ const actionPendingRef = React.useRef(false);
115
119
  // One clock per render pass: every relative time on screen must agree, and
116
120
  // re-reading it per row would let two rows a millisecond apart round to
117
121
  // different minutes.
@@ -237,9 +241,17 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
237
241
  setFilters(current => ({ ...current, ...update(current) }));
238
242
  topRef.current = 0;
239
243
  };
244
+ const runAction = (action) => {
245
+ if (actionPendingRef.current)
246
+ return;
247
+ actionPendingRef.current = true;
248
+ void action().finally(() => {
249
+ actionPendingRef.current = false;
250
+ });
251
+ };
240
252
  /** Run one mutation, report it, and re-list — reporting a failure either way. */
241
253
  const mutate = (action, done, failed) => {
242
- void (async () => {
254
+ runAction(async () => {
243
255
  let ok = false;
244
256
  let reason;
245
257
  try {
@@ -250,7 +262,7 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
250
262
  }
251
263
  report(ok ? done : reason === undefined ? failed : `${failed} · ${reason}`, ok ? 'info' : 'error');
252
264
  await reload();
253
- })();
265
+ });
254
266
  };
255
267
  const runDelete = (target) => mutate(() => channel.deleteSession(target.id), t('resume-deleted', { name: target.title.text }), t('resume-delete-failed', { name: target.title.text }));
256
268
  const runRename = (target, title) => mutate(() => channel.renameSessionTo(target.id, title), t('rename-done', { title }), t('resume-rename-failed', { name: target.title.text }));
@@ -259,7 +271,7 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
259
271
  // below, and deleting from a list that moved under us would be a
260
272
  // destructive action aimed at whatever happens to be there now.
261
273
  const ids = [...view.emptyIds];
262
- void (async () => {
274
+ runAction(async () => {
263
275
  let removed = 0;
264
276
  for (const id of ids) {
265
277
  try {
@@ -272,9 +284,11 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
272
284
  }
273
285
  report(t('session-cleaned', { n: removed }), 'info');
274
286
  await reload();
275
- })();
287
+ });
276
288
  };
277
289
  useInput((input, key) => {
290
+ if (actionPendingRef.current)
291
+ return;
278
292
  // A notice describes what the LAST action did; the next keystroke makes it
279
293
  // stale, so it goes as soon as the user acts again.
280
294
  if (notice !== undefined)
@@ -338,19 +352,20 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
338
352
  // that explanation to the conversation the user is not looking at, and
339
353
  // leave them staring at an unchanged transcript wondering what Enter
340
354
  // did. `resumeTo` reports its own reasons; this only has to stay put.
341
- void channel
342
- .resumeTo(target.id)
343
- .then((ok) => {
344
- if (ok) {
345
- channel.notify(t('resume-resumed'));
346
- onClose();
355
+ runAction(async () => {
356
+ try {
357
+ const ok = await channel.resumeTo(target.id);
358
+ if (ok) {
359
+ channel.notify(t('resume-resumed'));
360
+ onClose();
361
+ }
362
+ else {
363
+ setNotice({ text: t('session-resume-refused'), tone: 'error' });
364
+ }
347
365
  }
348
- else {
349
- setNotice({ text: t('session-resume-refused'), tone: 'error' });
366
+ catch (error) {
367
+ setNotice({ text: t('session-resume-failed', { err: message(error) }), tone: 'error' });
350
368
  }
351
- })
352
- .catch((error) => {
353
- setNotice({ text: t('session-resume-failed', { err: message(error) }), tone: 'error' });
354
369
  });
355
370
  }
356
371
  else if (key.escape) {
@@ -1 +1 @@
1
- {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../../src/sessions/view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA;AAEtE,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,wEAAwE;IACxE,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;CAChC;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,EAAE,cAK7B,CAAA;AAED,sCAAsC;AACtC,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC9E;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1F,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,CAAA;IACpC,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,yDAAyD;IACzD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAgBD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,GACtH,WAAW,CAuFb;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAK9F;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAKhG;AAED,+DAA+D;AAC/D,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAGhG;AAED,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAEjD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,SAAS,UAAU,EAAE,EAC3B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,MAAM,CAoBR;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAU1F"}
1
+ {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../../src/sessions/view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA;AAEtE,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,wEAAwE;IACxE,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;CAChC;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,EAAE,cAK7B,CAAA;AAED,sCAAsC;AACtC,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC9E;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1F,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,CAAA;IACpC,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,yDAAyD;IACzD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAgBD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,GACtH,WAAW,CAmGb;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAK9F;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAKhG;AAED,+DAA+D;AAC/D,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAGhG;AAED,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAEjD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,SAAS,UAAU,EAAE,EAC3B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,MAAM,CAoBR;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAU1F"}
@@ -85,7 +85,19 @@ export function buildView(sessions, filters, context) {
85
85
  const rows = [];
86
86
  let shown = 0;
87
87
  let lastProject;
88
- for (const session of visible.sort((left, right) => right.updatedAt - left.updatedAt)) {
88
+ const ordered = visible.sort((left, right) => right.updatedAt - left.updatedAt);
89
+ if (filters.allProjects) {
90
+ // Order project groups by their newest visible session, then keep every
91
+ // session from that project together in MRU order. A global MRU sort alone
92
+ // can interleave A/B/A and emit the same project header more than once.
93
+ const projectOrder = new Map();
94
+ for (const session of ordered) {
95
+ if (!projectOrder.has(session.cwd))
96
+ projectOrder.set(session.cwd, projectOrder.size);
97
+ }
98
+ ordered.sort((left, right) => projectOrder.get(left.cwd) - projectOrder.get(right.cwd) || right.updatedAt - left.updatedAt);
99
+ }
100
+ for (const session of ordered) {
89
101
  // Group headers only earn their line when more than one project is in
90
102
  // play; inside a single project they would repeat the same path forever.
91
103
  if (filters.allProjects && session.cwd !== lastProject) {
@@ -55,22 +55,36 @@ export declare function resolveTuiUpdateTarget(): Promise<TuiUpdateTarget>;
55
55
  * or blocks the interactive TUI.
56
56
  */
57
57
  export declare function checkForTuiUpdate(): Promise<TuiUpdateInfo | undefined>;
58
+ /** Build the profile-manager command, preferring a preflight-pinned version. */
59
+ export declare function tuiUpdatePluginArgs(profile: string, targetVersion?: string): string[];
60
+ /**
61
+ * The pnpm Windows tmp-rename race signature (issue #225): pnpm swaps a
62
+ * package directory via a `<name>_tmp_<pid>` staging dir, and a file lock or
63
+ * AV scan makes the scandir/rename fail with ENOENT/EPERM/EBUSY. The failure
64
+ * is transient — the identical command succeeds on retry — but the crashed
65
+ * run leaves a half-updated profile (manifest pins the old version while the
66
+ * lockfile already carries the new snapshot), which presents as "update did
67
+ * nothing" (#209). Genuine resolution errors never carry the `_tmp_<pid>`
68
+ * token, so matching both keeps the retry from masking real failures.
69
+ */
70
+ export declare function isTransientUpdateFailure(stderr: string): boolean;
58
71
  /**
59
72
  * Update the installed dsh-tui package and restart the same launcher while
60
73
  * preserving the active session. The TUI must already be unmounted before
61
74
  * this is called so pnpm output cannot corrupt the rendered terminal frame.
62
75
  *
63
- * `--latest` is required: `pnpm add` writes a caret range into the profile
64
- * manifest, and a plain `pnpm update` stays inside that range — with this
65
- * project's minor-per-release cadence the TUI would restart unchanged while
66
- * reporting success. The restart carries `DSH_TUI_UPDATED_FROM` so the new
67
- * process can warn when the version did not actually move (e.g. a mirror
68
- * registry still serving the old `latest`).
76
+ * When the preflight registry check resolved an exact target, pass that
77
+ * version to pnpm instead of resolving `latest` a second time. This avoids a
78
+ * stale mirror/dist-tag response between the check and install. If preflight
79
+ * failed, retain the `--latest` fallback: a plain `pnpm update` stays inside
80
+ * the manifest range and can restart unchanged across minor releases.
69
81
  *
70
82
  * @param sessionId - Session to resume in the replacement process.
71
83
  * @param profile - The dsh profile this TUI was launched with; updating any
72
84
  * other profile would leave the running install untouched.
85
+ * @param targetVersion - Exact version returned by the preflight registry
86
+ * check, or undefined when that check failed and pnpm should resolve latest.
73
87
  * @returns Exit codes for the update run and the replacement process.
74
88
  */
75
- export declare function updateTuiAndRestart(sessionId: string, profile: string): Promise<TuiUpdateResult>;
89
+ export declare function updateTuiAndRestart(sessionId: string, profile: string, targetVersion?: string): Promise<TuiUpdateResult>;
76
90
  //# sourceMappingURL=update.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAIlD,OAAO,EAAE,UAAU,EAAE,CAAA;AAQrB,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACf;AAED,4DAA4D;AAC5D,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAA;AAEvB,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,SAAS,CAiBxD;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,MAAM,GAAG,SAAS,CAahG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAW5C;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIzE;AA4BD;;;;GAIG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,eAAe,CAAC,CASvE;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAG5E;AAgDD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAuBtG"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAIlD,OAAO,EAAE,UAAU,EAAE,CAAA;AAQrB,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACf;AAED,4DAA4D;AAC5D,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAA;AAEvB,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,SAAS,CAiBxD;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,MAAM,GAAG,SAAS,CAahG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAW5C;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIzE;AA4BD;;;;GAIG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,eAAe,CAAC,CASvE;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAG5E;AA6DD,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAIrF;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,eAAe,CAAC,CAgD1B"}
@@ -155,9 +155,17 @@ function runProcess(command, args, options = {}) {
155
155
  : [command, args];
156
156
  const child = spawn(runCommand, runArgs, {
157
157
  env: options.env,
158
- stdio: 'inherit',
158
+ stdio: options.onStderr === undefined ? 'inherit' : ['inherit', 'inherit', 'pipe'],
159
159
  shell: useShell,
160
160
  });
161
+ if (options.onStderr !== undefined && child.stderr !== null) {
162
+ const onStderr = options.onStderr;
163
+ child.stderr.on('data', (chunk) => {
164
+ const text = chunk.toString('utf8');
165
+ process.stderr.write(text);
166
+ onStderr(text);
167
+ });
168
+ }
161
169
  const finish = (code) => {
162
170
  if (settled)
163
171
  return;
@@ -171,35 +179,77 @@ function runProcess(command, args, options = {}) {
171
179
  child.once('close', code => finish(code ?? 1));
172
180
  });
173
181
  }
182
+ /** Build the profile-manager command, preferring a preflight-pinned version. */
183
+ export function tuiUpdatePluginArgs(profile, targetVersion) {
184
+ return targetVersion === undefined
185
+ ? ['plugin', '--profile', profile, 'update', '--latest', PACKAGE_NAME]
186
+ : ['plugin', '--profile', profile, 'update', `${PACKAGE_NAME}@${targetVersion}`];
187
+ }
188
+ /**
189
+ * The pnpm Windows tmp-rename race signature (issue #225): pnpm swaps a
190
+ * package directory via a `<name>_tmp_<pid>` staging dir, and a file lock or
191
+ * AV scan makes the scandir/rename fail with ENOENT/EPERM/EBUSY. The failure
192
+ * is transient — the identical command succeeds on retry — but the crashed
193
+ * run leaves a half-updated profile (manifest pins the old version while the
194
+ * lockfile already carries the new snapshot), which presents as "update did
195
+ * nothing" (#209). Genuine resolution errors never carry the `_tmp_<pid>`
196
+ * token, so matching both keeps the retry from masking real failures.
197
+ */
198
+ export function isTransientUpdateFailure(stderr) {
199
+ return /ENOENT|EPERM|EBUSY/i.test(stderr) && /_tmp_\d+/i.test(stderr);
200
+ }
174
201
  /**
175
202
  * Update the installed dsh-tui package and restart the same launcher while
176
203
  * preserving the active session. The TUI must already be unmounted before
177
204
  * this is called so pnpm output cannot corrupt the rendered terminal frame.
178
205
  *
179
- * `--latest` is required: `pnpm add` writes a caret range into the profile
180
- * manifest, and a plain `pnpm update` stays inside that range — with this
181
- * project's minor-per-release cadence the TUI would restart unchanged while
182
- * reporting success. The restart carries `DSH_TUI_UPDATED_FROM` so the new
183
- * process can warn when the version did not actually move (e.g. a mirror
184
- * registry still serving the old `latest`).
206
+ * When the preflight registry check resolved an exact target, pass that
207
+ * version to pnpm instead of resolving `latest` a second time. This avoids a
208
+ * stale mirror/dist-tag response between the check and install. If preflight
209
+ * failed, retain the `--latest` fallback: a plain `pnpm update` stays inside
210
+ * the manifest range and can restart unchanged across minor releases.
185
211
  *
186
212
  * @param sessionId - Session to resume in the replacement process.
187
213
  * @param profile - The dsh profile this TUI was launched with; updating any
188
214
  * other profile would leave the running install untouched.
215
+ * @param targetVersion - Exact version returned by the preflight registry
216
+ * check, or undefined when that check failed and pnpm should resolve latest.
189
217
  * @returns Exit codes for the update run and the replacement process.
190
218
  */
191
- export async function updateTuiAndRestart(sessionId, profile) {
219
+ export async function updateTuiAndRestart(sessionId, profile, targetVersion) {
192
220
  const dsh = process.platform === 'win32' ? 'dsh.cmd' : 'dsh';
193
- const updateCode = await runProcess(dsh, [
194
- 'plugin',
195
- '--profile',
196
- profile,
197
- 'update',
198
- '--latest',
199
- PACKAGE_NAME,
200
- ], { shell: true });
221
+ const updateArgs = tuiUpdatePluginArgs(profile, targetVersion);
222
+ let updateStderr = '';
223
+ const capture = (chunk) => { updateStderr += chunk; };
224
+ let updateCode = await runProcess(dsh, updateArgs, { shell: true, onStderr: capture });
225
+ // Transient Windows tmp-rename race (issue #225): retry the identical
226
+ // command once — it succeeds on a clean second run, and only the
227
+ // `_tmp_<pid>` race signature qualifies, never a real resolution error.
228
+ if (updateCode !== 0 && isTransientUpdateFailure(updateStderr)) {
229
+ process.stderr.write('dsh-tui: transient pnpm failure (Windows tmp-rename race) — retrying once…\n');
230
+ updateStderr = '';
231
+ updateCode = await runProcess(dsh, updateArgs, { shell: true, onStderr: capture });
232
+ }
201
233
  if (updateCode !== 0)
202
234
  return { updateCode, restartCode: updateCode };
235
+ // Post-update verification (issue #225): pnpm can report success yet leave
236
+ // the profile half-updated (manifest old / lockfile new). Verify against
237
+ // the preflight target; a full `install` reconciles lockfile →
238
+ // node_modules, and if the mismatch survives, stop before restarting into
239
+ // a mixed state and hand the user the exact repair command instead.
240
+ if (targetVersion !== undefined) {
241
+ let installed = installedTuiVersion();
242
+ if (installed !== targetVersion) {
243
+ await runProcess(dsh, ['plugin', '--profile', profile, 'install'], { shell: true });
244
+ installed = installedTuiVersion();
245
+ }
246
+ if (installed !== targetVersion) {
247
+ process.stderr.write(`dsh-tui: update completed but the profile still runs ${installed ?? 'an unreadable version'} ` +
248
+ `(expected ${targetVersion}) — the profile is half-updated. Repair manually with:\n` +
249
+ ` dsh plugin --profile ${profile} add ${PACKAGE_NAME}@${targetVersion}\n`);
250
+ return { updateCode: 1, restartCode: 1 };
251
+ }
252
+ }
203
253
  const restartCode = await runProcess(process.execPath, [...process.execArgv, ...process.argv.slice(1)], {
204
254
  env: {
205
255
  ...process.env,
@@ -1 +1 @@
1
- {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../src/utils/paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH;;;;GAIG;AACH,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,qEAAqE;AACrE,eAAO,MAAM,QAAQ,QAA8B,CAAA;AAEnD,wEAAwE;AACxE,eAAO,MAAM,eAAe,QAA6B,CAAA;AAEzD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,GAAE,MAAwB,EAChC,MAAM,GAAE,MAAiB,GACxB,OAAO,CAIT;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAaxD,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,EAAE,CAEV"}
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../src/utils/paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH;;;;GAIG;AACH,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,qEAAqE;AACrE,eAAO,MAAM,QAAQ,QAA8B,CAAA;AAEnD,wEAAwE;AACxE,eAAO,MAAM,eAAe,QAA6B,CAAA;AAEzD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,GAAE,MAAwB,EAChC,MAAM,GAAE,MAAiB,GACxB,OAAO,CAOT;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAaxD,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,EAAE,CAEV"}
@@ -35,7 +35,10 @@ export const LEGACY_DATA_DIR = join(homeDir(), '.dsh-cc');
35
35
  export function migrateLegacyDataDir(legacy = LEGACY_DATA_DIR, target = DATA_DIR) {
36
36
  if (!existsSync(legacy) || existsSync(target))
37
37
  return false;
38
- cpSync(legacy, target, { recursive: true });
38
+ // `filter` forces the JS copy path: the native cpSync fast path fails with
39
+ // EIO (or crashes) on Windows when the destination lives under a home
40
+ // directory whose name contains non-ASCII characters (e.g. `C:\Users\米`).
41
+ cpSync(legacy, target, { recursive: true, filter: () => true });
39
42
  return true;
40
43
  }
41
44
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepseek-harness-tui/dsh-tui",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Claude Code style interactive TUI front door for DeepSeek Harness agents, built on the ported Ink core.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -57,16 +57,12 @@
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@deepseek-ai/cordis": "^4.0.1",
60
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6"
61
- },
62
- "dependencies": {
63
- "@alcalzone/ansi-tokenize": "^0.3.0",
64
60
  "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
65
61
  "@deepseek-ai/dsh-agent-instructions": "^0.1.0-rc.6",
66
62
  "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6",
67
- "@deepseek-ai/dsh-agent-tool-presentation": "^0.1.0-rc.6",
68
63
  "@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
69
64
  "@deepseek-ai/dsh-cordis-host-runner": "^0.1.0-rc.6",
65
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
70
66
  "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
71
67
  "@deepseek-ai/dsh-persona": "^0.1.0-rc.6",
72
68
  "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
@@ -83,14 +79,17 @@
83
79
  "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.6",
84
80
  "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.6",
85
81
  "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6",
86
- "@deepseek-ai/schemastery": "^3.18.1",
82
+ "@deepseek-ai/schemastery": "^3.18.1"
83
+ },
84
+ "dependencies": {
85
+ "@alcalzone/ansi-tokenize": "^0.3.0",
87
86
  "auto-bind": "^4.0.0 || ^5.0.0",
88
87
  "bidi-js": "^1.0.0",
89
88
  "chalk": "^5.0.0",
90
89
  "cli-boxes": "^3.0.0 || ^4.0.0",
91
90
  "cli-highlight": "^2.1.11",
92
91
  "code-excerpt": "^3.0.0 || ^4.0.0",
93
- "dsh-working-activity": "^0.2.4",
92
+ "dsh-working-activity": "^0.2.6",
94
93
  "emoji-regex": "^10.0.0",
95
94
  "figures": "^6.1.0",
96
95
  "get-east-asian-width": "^1.0.0",
@@ -113,26 +112,47 @@
113
112
  "@deepseek-ai/cordis": "^4.0.1",
114
113
  "@deepseek-ai/cordis-plugin-include": "^1.0.6",
115
114
  "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
115
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
116
+ "@deepseek-ai/dsh-agent-instructions": "^0.1.0-rc.6",
117
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6",
116
118
  "@deepseek-ai/dsh-atomic-write": "^0.1.0-rc.6",
117
119
  "@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
118
120
  "@deepseek-ai/dsh-brand": "^0.1.0-rc.6",
119
121
  "@deepseek-ai/dsh-code-runtime": "^0.1.0-rc.6",
122
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
123
+ "@deepseek-ai/dsh-cordis-host-runner": "^0.1.0-rc.6",
120
124
  "@deepseek-ai/dsh-fs": "^0.1.0-rc.6",
121
125
  "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
122
126
  "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
127
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
128
+ "@deepseek-ai/dsh-persona": "^0.1.0-rc.6",
123
129
  "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.6",
124
130
  "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.6",
125
131
  "@deepseek-ai/dsh-scope": "^0.1.0-rc.6",
132
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
126
133
  "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6",
127
134
  "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.0-rc.6",
128
135
  "@deepseek-ai/dsh-session-persistence-sqlite": "^0.1.0-rc.6",
129
136
  "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
137
+ "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
138
+ "@deepseek-ai/dsh-storage": "^0.1.0-rc.6",
139
+ "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.6",
140
+ "@deepseek-ai/dsh-storage-json": "^0.1.0-rc.6",
130
141
  "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6",
142
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
143
+ "@deepseek-ai/dsh-terminal": "^0.1.0-rc.6",
144
+ "@deepseek-ai/dsh-terminal-bash": "^0.1.0-rc.6",
131
145
  "@deepseek-ai/dsh-timeout": "^0.1.0-rc.6",
146
+ "@deepseek-ai/dsh-tool-ask-user": "^0.1.0-rc.6",
147
+ "@deepseek-ai/dsh-tool-bash-persistent": "^0.1.0-rc.6",
148
+ "@deepseek-ai/dsh-tool-cordis": "^0.1.0-rc.6",
132
149
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
133
150
  "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6",
134
151
  "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.6",
152
+ "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.6",
135
153
  "@deepseek-ai/dsh-web-app": "0.1.0-rc.6",
154
+ "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6",
155
+ "@deepseek-ai/schemastery": "^3.18.1",
136
156
  "@types/lodash-es": "^4.17.0",
137
157
  "@types/node": "^22.0.0",
138
158
  "@types/react": "^19.0.0",
@@ -152,11 +172,15 @@
152
172
  "clean": "node scripts/clean-lib.mjs",
153
173
  "compile": "npm run clean && tsc -p tsconfig.json",
154
174
  "build": "npm run compile && npm run verify:build",
175
+ "dev": "node scripts/dev-test.mjs",
176
+ "dev:copy-config": "node scripts/dev-copy-config.mjs",
177
+ "dev:test": "node scripts/verify-dev-command.mjs && node scripts/dev-test.mjs --no-launch",
155
178
  "tui": "node --import tsx/esm scripts/run.ts",
156
179
  "smoke": "node --import tsx/esm scripts/smoke.tsx",
157
- "verify:build": "npm run verify:boundary && npm run verify:contract && npm run verify:patch-surface && npm run verify:packaged-presets && npm run verify:minimal-preset-tools && npm run verify:liangshen-bootstrap",
180
+ "verify:build": "npm run verify:boundary && npm run verify:contract && npm run verify:manifest-deps && npm run verify:patch-surface && npm run verify:packaged-presets && npm run verify:minimal-preset-tools && npm run verify:liangshen-bootstrap",
158
181
  "verify:boundary": "node --import tsx/esm scripts/verify-adapter-boundary.ts",
159
182
  "verify:contract": "node --import tsx/esm scripts/verify-upstream-contract.ts",
183
+ "verify:manifest-deps": "node --import tsx/esm scripts/verify-manifest-deps.ts",
160
184
  "verify:patch-surface": "node --import tsx/esm scripts/verify-patch-surface.ts",
161
185
  "verify:packaged-presets": "node scripts/verify-packaged-presets.mjs",
162
186
  "verify:minimal-preset-tools": "node scripts/verify-minimal-preset-tools.mjs",