dsh-code 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.en.md +5 -5
  2. package/README.md +5 -5
  3. package/lib/index.mjs +6679 -5232
  4. package/lib/startup.mjs +1 -1
  5. package/lib/{theme-7u5Qo3dF.mjs → theme-B3orFUYz.mjs} +8 -0
  6. package/lib/types/app.d.ts +32 -45
  7. package/lib/types/attachments.d.ts +16 -7
  8. package/lib/types/completion.d.ts +29 -0
  9. package/lib/types/composer.d.ts +150 -0
  10. package/lib/types/git-workflow.d.ts +6 -0
  11. package/lib/types/index.d.ts +6 -188
  12. package/lib/types/locales/en.d.ts +65 -5
  13. package/lib/types/{authorization-panel.d.ts → panels/authorization-panel.d.ts} +6 -1
  14. package/lib/types/panels/completion-panel.d.ts +13 -0
  15. package/lib/types/panels/interaction-bars.d.ts +38 -0
  16. package/lib/types/{kernel-panels.d.ts → panels/kernel-panels.d.ts} +19 -38
  17. package/lib/types/{language-panel.d.ts → panels/language-panel.d.ts} +1 -1
  18. package/lib/types/panels/model-panels.d.ts +86 -0
  19. package/lib/types/{theme-panel.d.ts → panels/theme-panel.d.ts} +1 -1
  20. package/lib/types/{update-panel.d.ts → panels/update-panel.d.ts} +22 -1
  21. package/lib/types/provider-settings.d.ts +11 -0
  22. package/lib/types/render/inspector.d.ts +8 -0
  23. package/lib/types/render/status.d.ts +4 -1
  24. package/lib/types/render/text.d.ts +4 -0
  25. package/lib/types/runner/harness-gate.d.ts +83 -0
  26. package/lib/types/runner/input-history.d.ts +31 -0
  27. package/lib/types/runner/mode-cycle.d.ts +44 -0
  28. package/lib/types/runner/preferences.d.ts +40 -0
  29. package/lib/types/runner/quit.d.ts +27 -0
  30. package/lib/types/runner/search-rows.d.ts +38 -0
  31. package/lib/types/runner/session-io.d.ts +46 -0
  32. package/lib/types/runner/session-target.d.ts +42 -0
  33. package/lib/types/runner/startup-config.d.ts +33 -0
  34. package/lib/types/runner/submissions.d.ts +87 -0
  35. package/lib/types/session/attach.d.ts +39 -0
  36. package/lib/types/{history.d.ts → session/history.d.ts} +10 -0
  37. package/lib/types/{session-directory.d.ts → session/session-directory.d.ts} +25 -1
  38. package/lib/types/{session-switch.d.ts → session/session-switch.d.ts} +8 -0
  39. package/lib/types/{store.d.ts → session/store.d.ts} +1 -1
  40. package/lib/types/{subagents.d.ts → session/subagents.d.ts} +8 -1
  41. package/lib/types/settings-file.d.ts +10 -0
  42. package/lib/types/{panel-accent.d.ts → ui/panel-accent.d.ts} +1 -1
  43. package/lib/types/ui/panel-gap.d.ts +6 -0
  44. package/lib/types/ui/query-editor.d.ts +10 -0
  45. package/lib/types/ui/styled-rows.d.ts +8 -0
  46. package/lib/types/{terminal-title.d.ts → ui/terminal-title.d.ts} +1 -1
  47. package/lib/types/ui/ui-contract.d.ts +12 -0
  48. package/lib/types/ui/use-frames.d.ts +6 -0
  49. package/lib/types/ui/use-stable-input.d.ts +7 -0
  50. package/lib/types/version.d.ts +2 -0
  51. package/package.json +7 -5
  52. package/src/app.ts +707 -4108
  53. package/src/attachments.ts +65 -19
  54. package/src/completion.ts +117 -0
  55. package/src/composer.ts +1956 -0
  56. package/src/git-workflow.ts +18 -0
  57. package/src/index.ts +164 -645
  58. package/src/input-split.ts +24 -4
  59. package/src/internals.ts +1 -1
  60. package/src/locales/en.ts +66 -5
  61. package/src/locales/zh.ts +66 -5
  62. package/src/{authorization-panel.ts → panels/authorization-panel.ts} +30 -8
  63. package/src/panels/completion-panel.ts +79 -0
  64. package/src/panels/interaction-bars.ts +567 -0
  65. package/src/{kernel-panels.ts → panels/kernel-panels.ts} +142 -90
  66. package/src/{language-panel.ts → panels/language-panel.ts} +4 -4
  67. package/src/panels/model-panels.ts +1021 -0
  68. package/src/{theme-panel.ts → panels/theme-panel.ts} +5 -5
  69. package/src/{update-panel.ts → panels/update-panel.ts} +117 -10
  70. package/src/provider-settings.ts +38 -0
  71. package/src/rainbow.ts +13 -3
  72. package/src/render/inspector.ts +23 -0
  73. package/src/render/status.ts +80 -29
  74. package/src/render/text.ts +10 -1
  75. package/src/runner/harness-gate.ts +168 -0
  76. package/src/runner/input-history.ts +77 -0
  77. package/src/runner/mode-cycle.ts +49 -0
  78. package/src/runner/preferences.ts +67 -0
  79. package/src/runner/quit.ts +53 -0
  80. package/src/runner/search-rows.ts +55 -0
  81. package/src/runner/session-io.ts +206 -0
  82. package/src/runner/session-target.ts +81 -0
  83. package/src/runner/startup-config.ts +54 -0
  84. package/src/runner/submissions.ts +157 -0
  85. package/src/session/attach.ts +87 -0
  86. package/src/{fork.ts → session/fork.ts} +11 -7
  87. package/src/{history.ts → session/history.ts} +14 -0
  88. package/src/{session-directory.ts → session/session-directory.ts} +83 -4
  89. package/src/{session-switch.ts → session/session-switch.ts} +14 -0
  90. package/src/{store.ts → session/store.ts} +20 -2
  91. package/src/{subagents.ts → session/subagents.ts} +12 -1
  92. package/src/settings-file.ts +19 -1
  93. package/src/{panel-accent.ts → ui/panel-accent.ts} +1 -1
  94. package/src/ui/panel-gap.ts +9 -0
  95. package/src/ui/query-editor.ts +16 -0
  96. package/src/ui/styled-rows.ts +124 -0
  97. package/src/{terminal-title.ts → ui/terminal-title.ts} +1 -1
  98. package/src/ui/ui-contract.ts +10 -0
  99. package/src/ui/use-frames.ts +23 -0
  100. package/src/ui/use-stable-input.ts +17 -0
  101. package/src/version.ts +5 -0
  102. /package/lib/types/{fork.d.ts → session/fork.d.ts} +0 -0
@@ -62,10 +62,30 @@ export function createKeypressSplitter(): KeypressSplitter {
62
62
  }
63
63
  const head = buffer[0]
64
64
  if (head !== '\x1b') {
65
- // Plain bytes key one at a time; a surrogate pair is one grapheme
66
- // and must not split into two lone surrogates.
67
- const pair = head >= '\uD800' && head <= '\uDBFF' && buffer[1] !== undefined
68
- const take = pair ? 2 : 1
65
+ // C0 / DEL stay one key so a coalesced ` \r` is still space then
66
+ // Enter. A printable run (file drag or unbracketed path paste on
67
+ // any platform, including CJK) must stay ONE unit: splitting a
68
+ // long path into tens of setStates overflows React's update depth
69
+ // and never reaches the attachment parser. Typed keys still
70
+ // arrive as one-byte chunks, so this only batches what the OS
71
+ // already coalesced.
72
+ if (head < ' ' || head === '\x7f') {
73
+ units.push(head)
74
+ buffer = buffer.slice(1)
75
+ continue
76
+ }
77
+ let take = 0
78
+ while (take < buffer.length) {
79
+ const ch = buffer[take]
80
+ if (ch === '\x1b' || ch < ' ' || ch === '\x7f') break
81
+ if (ch >= '\uD800' && ch <= '\uDBFF') {
82
+ if (buffer[take + 1] === undefined) break
83
+ take += 2
84
+ continue
85
+ }
86
+ take += 1
87
+ }
88
+ if (take === 0) break
69
89
  units.push(buffer.slice(0, take))
70
90
  buffer = buffer.slice(take)
71
91
  continue
package/src/internals.ts CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  shouldEnableKeyboardEnhancement,
20
20
  } from './keyboard.ts'
21
21
  import { createSplitStdin } from './input-split.ts'
22
- import { ensureVsCodeTabTitleSetting } from './terminal-title.ts'
22
+ import { ensureVsCodeTabTitleSetting } from './ui/terminal-title.ts'
23
23
 
24
24
  /** A mounted terminal app instance; the runner owns unmount ordering. */
25
25
  export interface TuiMount {
package/src/locales/en.ts CHANGED
@@ -63,7 +63,38 @@ export const en = {
63
63
  'search.footer': '↑↓ move · enter search/resume · esc close',
64
64
 
65
65
  // /statusline panel
66
+ 'header.hint': '/help · Esc interrupt · Ctrl+C quit',
67
+ 'header.hintResumed': 'resumed · /help · Esc interrupt',
68
+
66
69
  'statusline.title': '/statusline · items apply to the live status line below',
70
+ 'statusline.item.model': 'model',
71
+ 'statusline.item.model.desc': 'provider/model serving this session',
72
+ 'statusline.item.cwd': 'cwd',
73
+ 'statusline.item.cwd.desc': 'working-directory basename',
74
+ 'statusline.item.mode': 'mode',
75
+ 'statusline.item.mode.desc': 'agent preset composing the session',
76
+ 'statusline.item.branch': 'branch',
77
+ 'statusline.item.branch.desc': 'git branch inside a repository',
78
+ 'statusline.item.context': 'context',
79
+ 'statusline.item.context.desc': 'context-window occupancy meter',
80
+ 'statusline.item.permission': 'permission',
81
+ 'statusline.item.permission.desc': 'permission preset badge with cycle hint',
82
+ 'statusline.item.plan': 'plan',
83
+ 'statusline.item.plan.desc': 'plan-mode state mark',
84
+ 'statusline.item.turns': 'turns',
85
+ 'statusline.item.turns.desc': 'turn and step counters',
86
+ 'statusline.item.durations': 'durations',
87
+ 'statusline.item.durations.desc': 'llm/ttft/decode/tool wall time',
88
+ 'statusline.item.cache': 'cache',
89
+ 'statusline.item.cache.desc': 'cache-hit share of billed input',
90
+ 'statusline.item.tokens': 'tokens',
91
+ 'statusline.item.tokens.desc': 'cumulative input/output tokens',
92
+ 'statusline.item.title': 'title',
93
+ 'statusline.item.title.desc': 'session title or short id',
94
+ 'statusline.item.goal': 'goal',
95
+ 'statusline.item.goal.desc': 'live goal phase and round progress',
96
+ 'statusline.item.sandbox': 'sandbox',
97
+ 'statusline.item.sandbox.desc': 'divergent sandbox-mode override',
67
98
 
68
99
  // /schedule panel
69
100
  'schedule.title': '/schedule · {count} active reminder',
@@ -72,8 +103,15 @@ export const en = {
72
103
 
73
104
  // Transcript document panel (/resume details)
74
105
  'document.loading': 'loading transcript…',
106
+ 'attach.bar': 'observing · {label} · r reload · esc/ctrl+d back',
107
+ 'attach.loading': 'loading {label}\'s conversation…',
108
+ 'attach.status': 'subagent {label} · {id} · {turns} turns',
109
+ 'notice.attachUnavailable': 'subagent attachment is unavailable in this profile',
75
110
  'document.compact': 'transcript · esc close',
76
- 'document.footer': 'lines {from}-{to}/{total} · ↑↓/pg/g/G · t/esc close',
111
+ 'document.expanded': 'expanded',
112
+ 'document.refreshHint': 'r refresh',
113
+ 'document.footerExpanded': 'lines {from}-{to}/{total} · ↑↓/pg/g/G{refresh} · esc/ctrl+d back',
114
+ 'document.footer': 'lines {from}-{to}/{total} · ↑↓/pg/g/G · enter/f expand · t/esc close',
77
115
 
78
116
  // /review picker
79
117
  'review.picker.title': '/review · choose a target',
@@ -227,12 +265,18 @@ export const en = {
227
265
  'status.label.out': 'out',
228
266
  'status.label.mode': '/mode',
229
267
  'status.label.context': 'context',
268
+ 'status.label.sandbox': 'sandbox',
269
+ 'status.plan.on': 'plan on',
270
+ 'status.plan.mark': '⧉ plan',
271
+ 'status.goal.round': '◎ round {current}/{max}',
272
+ 'status.goal.phase': '◎ {phase}',
230
273
 
231
274
  // Frozen band
232
275
  'frozen.keysGoTo': 'keys go to {owner} · esc {action}',
233
276
  'frozen.action.rejects': 'rejects',
234
277
  'frozen.action.cancels': 'cancels',
235
278
  'frozen.action.closes': 'closes',
279
+ 'frozen.action.waits': 'waits',
236
280
 
237
281
  // Relative time (panel list timestamps)
238
282
  'time.justNow': 'now',
@@ -248,6 +292,7 @@ export const en = {
248
292
  'notice.themeSaveFailed': 'theme save failed: {message}',
249
293
  'notice.languageSaveFailed': 'language save failed: {message}',
250
294
  'notice.alreadyActive': 'that session is already active',
295
+ 'notice.permissionPresetsUnmounted': 'permission presets are not mounted in this composition',
251
296
  'notice.queueCancelled': 'queued message cancelled',
252
297
  'notice.queueActionFailed': 'queue action failed: {message}',
253
298
  'notice.queueUnavailable': 'queued message is no longer pending',
@@ -262,6 +307,8 @@ export const en = {
262
307
  'notice.usage.animation': 'usage: /animation [on|off]',
263
308
  'notice.usage.language': 'usage: /language [en|zh]',
264
309
  'notice.usage.rainbow': 'usage: /rainbow [seed]',
310
+ 'notice.usage.bareCommand': 'usage: /{name}',
311
+ 'notice.switchInProgress': 'switching sessions — send again once the new session is up, or /resume cancel',
265
312
  'notice.imageCancelled': 'image submission cancelled',
266
313
  'notice.themeConfigUnreadable': 'theme config unreadable, using dark: {message}',
267
314
  'notice.languageConfigUnreadable': 'language config unreadable, using english: {message}',
@@ -292,6 +339,13 @@ export const en = {
292
339
  'notice.resumeFailed': 'resume failed: {message}',
293
340
  'notice.forkFailed': 'fork failed: {message}',
294
341
  'notice.sessionSwitchFailed': 'session switch failed: {message}',
342
+ 'notice.sessionCreated': 'created {id} · mode {mode}',
343
+ 'notice.sessionResumed': 'resumed {id} · mode {mode}',
344
+ 'notice.sessionSwitchedDirty': 'switched to {id}, but {detail}',
345
+ 'notice.flushFailed': 'previous session flush failed: {message}',
346
+ 'notice.agentReleaseFailed': 'previous agent release failed: {message}',
347
+ 'notice.copyEmpty': 'nothing to copy yet',
348
+ 'notice.copied': 'copied latest response',
295
349
  'notice.switchQueued': 'will switch to {label} when the current turn finishes · /resume cancel to abort',
296
350
  'notice.reviewStarted': 'review started under read-only permissions',
297
351
  'notice.reviewFailed': 'review failed: {message}',
@@ -345,7 +399,8 @@ export const en = {
345
399
  'panel.footer.chooseSelect': '↑↓ choose · enter select · r refresh · esc close',
346
400
  'panel.footer.inspectDetails': '↑↓ inspect · enter details · r refresh · esc close',
347
401
  'panel.footer.inspectRefresh': '↑↓ inspect · r refresh · esc close',
348
- 'panel.footer.resume': 'tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · d delete · enter resume',
402
+ 'panel.footer.resume': 'tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · enter resume',
403
+ 'panel.footer.delete': 'tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · enter delete · esc close',
349
404
  'panel.footer.transcript': 'lines {from}-{to}/{total} · ↑↓/pg/g/G · t/esc close',
350
405
  'panel.footer.history': '↑↓ move · g/G ends · enter fill · esc close',
351
406
  'panel.footer.review': '↑↓ move · enter select · esc back · q close',
@@ -357,6 +412,7 @@ export const en = {
357
412
  'panel.jobs.title': '/jobs · background tasks · {count}',
358
413
  'panel.resume.deleteTitle': 'permanently delete {target}? this cannot be undone · subagent threads go too',
359
414
  'panel.resume.title': '/resume{mode}{search} · {toolbar}',
415
+ 'panel.delete.title': '/delete{search} · {toolbar}',
360
416
  'panel.noActiveReminders': 'no active reminders — the model schedules via schedule_create',
361
417
  'panel.notMounted': 'not mounted',
362
418
  'panel.broken': 'broken: {message}',
@@ -407,7 +463,6 @@ export const en = {
407
463
  'panel.provider.notConfiguredDivider': '── not configured ──',
408
464
  'panel.provider.title': '/model — providers',
409
465
  'panel.provider.titleCount': '/model — providers · {index}/{total}',
410
- 'panel.provider.state': '{route} · {value}',
411
466
  'panel.provider.key': 'key {value}',
412
467
  'panel.provider.authConfigured': 'provider auth',
413
468
  'panel.provider.noKey': 'key missing',
@@ -422,7 +477,7 @@ export const en = {
422
477
  'panel.provider.readOnlyLogin': 'this login record is read-only',
423
478
  'panel.provider.notManaged': 'this provider is not managed by Harness settings',
424
479
  'panel.providers.compact': '/model providers · enter configure · d remove key · esc back',
425
- 'panel.providers.footer': '↑↓ move · enter configure · l login · o logout · d remove key · x remove provider · r retry · esc back',
480
+ 'panel.providers.footer': '↑↓ move · enter configure · d remove key · x remove provider · r retry · esc back',
426
481
  'panel.update.compact': '/update · {summary}',
427
482
  'panel.update.checking': 'checking…',
428
483
  'panel.update.probeFailed': 'probe failed',
@@ -450,8 +505,14 @@ export const en = {
450
505
  'panel.setup.copyInto': 'copy verbatim into {id}',
451
506
  'panel.setup.copyTitle': '/model — copy efforts',
452
507
  'panel.setup.copyFooter': '↑↓ move · enter copy · esc back',
508
+ 'panel.setup.mode.subscribe': 'Subscription',
509
+ 'panel.setup.mode.key': 'Key',
510
+ 'panel.setup.mode.login.plan': 'signed in · plan',
511
+ 'panel.setup.mode.login.none': 'not signed in',
512
+ 'panel.setup.mode.login.running': 'signing in…',
513
+ 'panel.setup.mode.overridden': '⚠ the settings key overrides the plan sign-in',
453
514
  'panel.setup.title': '/model — configure {provider}',
454
- 'panel.setup.footer': '↑↓ move · ←→ in/out · space remove · e efforts · c copy efforts · tab discover · enter save · esc back',
515
+ 'panel.setup.footer': '↑↓ move · ←→ in/out · space remove · e efforts · c copy efforts · tab per channel (subscription = web sign-in, key = discover) · enter save · esc back',
455
516
  'panel.discovery.compact': 'model discovery · terminal too small · esc back',
456
517
  'panel.discovery.loading': 'discovering models…',
457
518
  'panel.discovery.empty': 'the endpoint advertised no models; add ids by hand on the setup page',
package/src/locales/zh.ts CHANGED
@@ -65,7 +65,38 @@ export const zh: MessageCatalog = {
65
65
  'search.footer': '↑↓ 移动 · 回车 搜索/恢复 · esc 关闭',
66
66
 
67
67
  // /statusline panel
68
+ 'header.hint': '/help · Esc 中断 · Ctrl+C 退出',
69
+ 'header.hintResumed': '已恢复 · /help · Esc 中断',
70
+
68
71
  'statusline.title': '/statusline · 选项实时应用到下方状态栏',
72
+ 'statusline.item.model': '模型',
73
+ 'statusline.item.model.desc': '本会话使用的 provider/model',
74
+ 'statusline.item.cwd': '目录',
75
+ 'statusline.item.cwd.desc': '工作目录基名',
76
+ 'statusline.item.mode': '模式',
77
+ 'statusline.item.mode.desc': '当前 agent 预设',
78
+ 'statusline.item.branch': '分支',
79
+ 'statusline.item.branch.desc': '仓库内 git 分支',
80
+ 'statusline.item.context': '上下文',
81
+ 'statusline.item.context.desc': '上下文窗口占用',
82
+ 'statusline.item.permission': '权限',
83
+ 'statusline.item.permission.desc': '权限预设徽章与循环提示',
84
+ 'statusline.item.plan': '计划',
85
+ 'statusline.item.plan.desc': '计划模式标记',
86
+ 'statusline.item.turns': '回合',
87
+ 'statusline.item.turns.desc': '回合与步骤计数',
88
+ 'statusline.item.durations': '耗时',
89
+ 'statusline.item.durations.desc': '模型/首字/解码/工具墙钟',
90
+ 'statusline.item.cache': '缓存',
91
+ 'statusline.item.cache.desc': '计费输入的缓存命中占比',
92
+ 'statusline.item.tokens': '用量',
93
+ 'statusline.item.tokens.desc': '累计输入/输出 token',
94
+ 'statusline.item.title': '标题',
95
+ 'statusline.item.title.desc': '会话标题或短 id',
96
+ 'statusline.item.goal': '目标',
97
+ 'statusline.item.goal.desc': '进行中的目标阶段与轮次',
98
+ 'statusline.item.sandbox': '沙箱',
99
+ 'statusline.item.sandbox.desc': '与预设不同的沙箱覆盖',
69
100
 
70
101
  // /schedule panel
71
102
  'schedule.title': '/schedule · {count} 条活动提醒',
@@ -74,8 +105,15 @@ export const zh: MessageCatalog = {
74
105
 
75
106
  // Transcript document panel (/resume details)
76
107
  'document.loading': '转录加载中…',
108
+ 'attach.bar': '观察模式 · {label} · r 重新载入 · esc/ctrl+d 返回',
109
+ 'attach.loading': '正在载入 {label} 的子会话…',
110
+ 'attach.status': '子代理 {label} · {id} · {turns} 回合',
111
+ 'notice.attachUnavailable': '当前配置无法附加子代理会话',
77
112
  'document.compact': '转录 · esc 关闭',
78
- 'document.footer': '行 {from}-{to}/{total} · ↑↓/pg/g/G · t/esc 关闭',
113
+ 'document.expanded': '全屏',
114
+ 'document.refreshHint': 'r 刷新',
115
+ 'document.footerExpanded': '行 {from}-{to}/{total} · ↑↓/pg/g/G{refresh} · esc/ctrl+d 返回',
116
+ 'document.footer': '行 {from}-{to}/{total} · ↑↓/pg/g/G · enter/f 全屏 · t/esc 关闭',
79
117
 
80
118
  // /review picker
81
119
  'review.picker.title': '/review · 选择审查目标',
@@ -229,12 +267,18 @@ export const zh: MessageCatalog = {
229
267
  'status.label.out': '出',
230
268
  'status.label.mode': '/mode',
231
269
  'status.label.context': '上下文',
270
+ 'status.label.sandbox': '沙箱',
271
+ 'status.plan.on': '计划开',
272
+ 'status.plan.mark': '⧉ 计划',
273
+ 'status.goal.round': '◎ 第 {current}/{max} 轮',
274
+ 'status.goal.phase': '◎ {phase}',
232
275
 
233
276
  // Frozen band
234
277
  'frozen.keysGoTo': '按键交给{owner} · esc {action}',
235
278
  'frozen.action.rejects': '拒绝',
236
279
  'frozen.action.cancels': '取消',
237
280
  'frozen.action.closes': '关闭',
281
+ 'frozen.action.waits': '等待',
238
282
 
239
283
  // Relative time (panel list timestamps)
240
284
  'time.justNow': '刚刚',
@@ -250,6 +294,7 @@ export const zh: MessageCatalog = {
250
294
  'notice.themeSaveFailed': '主题保存失败:{message}',
251
295
  'notice.languageSaveFailed': '语言保存失败:{message}',
252
296
  'notice.alreadyActive': '该会话已是当前会话',
297
+ 'notice.permissionPresetsUnmounted': '当前组合未挂载权限预设',
253
298
  'notice.queueCancelled': '已取消排队消息',
254
299
  'notice.queueActionFailed': '队列操作失败:{message}',
255
300
  'notice.queueUnavailable': '该消息已不在队列中',
@@ -264,6 +309,8 @@ export const zh: MessageCatalog = {
264
309
  'notice.usage.animation': '用法:/animation [on|off]',
265
310
  'notice.usage.language': '用法:/language [en|zh]',
266
311
  'notice.usage.rainbow': '用法:/rainbow [seed]',
312
+ 'notice.usage.bareCommand': '用法:/{name}',
313
+ 'notice.switchInProgress': '正在切换会话 —— 等新会话就绪后再发送,或用 /resume cancel 取消',
267
314
  'notice.imageCancelled': '图片提交已取消',
268
315
  'notice.themeConfigUnreadable': '主题配置无法读取,使用暗色:{message}',
269
316
  'notice.languageConfigUnreadable': '语言配置无法读取,使用英文:{message}',
@@ -294,6 +341,13 @@ export const zh: MessageCatalog = {
294
341
  'notice.resumeFailed': '恢复失败:{message}',
295
342
  'notice.forkFailed': '分叉失败:{message}',
296
343
  'notice.sessionSwitchFailed': '会话切换失败:{message}',
344
+ 'notice.sessionCreated': '已创建 {id} · 模式 {mode}',
345
+ 'notice.sessionResumed': '已恢复 {id} · 模式 {mode}',
346
+ 'notice.sessionSwitchedDirty': '已切换到 {id},但 {detail}',
347
+ 'notice.flushFailed': '上一会话刷新失败:{message}',
348
+ 'notice.agentReleaseFailed': '上一代理释放失败:{message}',
349
+ 'notice.copyEmpty': '还没有可复制的回复',
350
+ 'notice.copied': '已复制最近一条回复',
297
351
  'notice.switchQueued': '当前回合结束后将切换到 {label} · 可用 /resume cancel 取消',
298
352
  'notice.reviewStarted': '审查已在只读权限下开始',
299
353
  'notice.reviewFailed': '审查失败:{message}',
@@ -347,7 +401,8 @@ export const zh: MessageCatalog = {
347
401
  'panel.footer.chooseSelect': '↑↓ 移动 · 回车 选择 · r 刷新 · esc 关闭',
348
402
  'panel.footer.inspectDetails': '↑↓ 查看 · 回车 详情 · r 刷新 · esc 关闭',
349
403
  'panel.footer.inspectRefresh': '↑↓ 查看 · r 刷新 · esc 关闭',
350
- 'panel.footer.resume': 'tab/←→ 筛选 · ↑↓/pg 翻页 · ctrl+f 搜索 · e 详情 · t 转录 · d 删除 · 回车恢复',
404
+ 'panel.footer.resume': 'tab/←→ 筛选 · ↑↓/pg 翻页 · ctrl+f 搜索 · e 详情 · t 转录 · 回车恢复',
405
+ 'panel.footer.delete': 'tab/←→ 筛选 · ↑↓/pg 翻页 · ctrl+f 搜索 · e 详情 · t 转录 · 回车删除 · esc 关闭',
351
406
  'panel.footer.transcript': '行 {from}-{to}/{total} · ↑↓/pg/g/G · t/esc 关闭',
352
407
  'panel.footer.history': '↑↓ 移动 · g/G 首尾 · 回车填入 · esc 关闭',
353
408
  'panel.footer.review': '↑↓ 移动 · 回车选择 · esc 返回 · q 关闭',
@@ -359,6 +414,7 @@ export const zh: MessageCatalog = {
359
414
  'panel.jobs.title': '/jobs · 后台任务 · {count}',
360
415
  'panel.resume.deleteTitle': '永久删除 {target}?此操作无法撤销,子代理线程也会删除',
361
416
  'panel.resume.title': '/resume{mode}{search} · {toolbar}',
417
+ 'panel.delete.title': '/delete{search} · {toolbar}',
362
418
  'panel.noActiveReminders': '暂无活动提醒——模型通过 schedule_create 创建',
363
419
  'panel.notMounted': '未加载',
364
420
  'panel.broken': '异常:{message}',
@@ -409,7 +465,6 @@ export const zh: MessageCatalog = {
409
465
  'panel.provider.notConfiguredDivider': '── 未配置 ──',
410
466
  'panel.provider.title': '/model — 供应商',
411
467
  'panel.provider.titleCount': '/model — 供应商 · {index}/{total}',
412
- 'panel.provider.state': '{route} · {value}',
413
468
  'panel.provider.key': '密钥 {value}',
414
469
  'panel.provider.authConfigured': '供应商认证',
415
470
  'panel.provider.noKey': '缺少密钥',
@@ -424,7 +479,7 @@ export const zh: MessageCatalog = {
424
479
  'panel.provider.readOnlyLogin': '该登录记录为只读',
425
480
  'panel.provider.notManaged': '该供应商不受 Harness settings 管理',
426
481
  'panel.providers.compact': '/model providers · 回车配置 · d 删除密钥 · esc 返回',
427
- 'panel.providers.footer': '↑↓ 移动 · 回车配置 · l 登录 · o 登出 · d 删除密钥 · x 删除供应商 · r 重试 · esc 返回',
482
+ 'panel.providers.footer': '↑↓ 移动 · 回车配置 · d 删除密钥 · x 删除供应商 · r 重试 · esc 返回',
428
483
  'panel.update.compact': '/update · {summary}',
429
484
  'panel.update.checking': '检查中…',
430
485
  'panel.update.probeFailed': '检查失败',
@@ -452,8 +507,14 @@ export const zh: MessageCatalog = {
452
507
  'panel.setup.copyInto': '原样复制到 {id}',
453
508
  'panel.setup.copyTitle': '/model — 复制推理档位',
454
509
  'panel.setup.copyFooter': '↑↓ 移动 · 回车复制 · esc 返回',
510
+ 'panel.setup.mode.subscribe': '订阅',
511
+ 'panel.setup.mode.key': 'Key',
512
+ 'panel.setup.mode.login.plan': '已登录 · 套餐',
513
+ 'panel.setup.mode.login.none': '未登录',
514
+ 'panel.setup.mode.login.running': '登录中…',
515
+ 'panel.setup.mode.overridden': '⚠ 设置中的 Key 覆盖订阅登录',
455
516
  'panel.setup.title': '/model — 配置 {provider}',
456
- 'panel.setup.footer': '↑↓ 移动 · ←→ 输入/输出 · 空格移除 · e 推理档位 · c 复制推理档位 · tab 发现 · 回车保存 · esc 返回',
517
+ 'panel.setup.footer': '↑↓ 移动 · ←→ 输入/输出 · 空格移除 · e 推理档位 · c 复制推理档位 · tab 按当前方式(订阅=网页登录 / Key=发现) · 回车保存 · esc 返回',
457
518
  'panel.discovery.compact': '模型发现 · 终端过窄 · esc 返回',
458
519
  'panel.discovery.loading': '正在发现模型…',
459
520
  'panel.discovery.empty': '端点未提供模型;请在配置页手动添加 ID',
@@ -10,12 +10,12 @@ import {
10
10
  type AuthorizationStatus,
11
11
  } from '@deepseek-ai/dsh-authorization'
12
12
  import type { CredentialKey } from '@deepseek-ai/dsh-credentials'
13
- import type { ProviderAuthorizationRow } from './authorization.ts'
14
- import { panelViewport } from './render/inspector.ts'
15
- import { displayText, singleLineText, truncateColumns } from './render/text.ts'
16
- import { panelAccent } from './panel-accent.ts'
17
- import { getPalette, inkColor } from './theme.ts'
18
- import { t } from './i18n.ts'
13
+ import type { ProviderAuthorizationRow } from '../authorization.ts'
14
+ import { panelViewport } from '../render/inspector.ts'
15
+ import { displayText, singleLineText, truncateColumns } from '../render/text.ts'
16
+ import { panelAccent } from '../ui/panel-accent.ts'
17
+ import { getPalette, inkColor } from '../theme.ts'
18
+ import { t } from '../i18n.ts'
19
19
 
20
20
  interface PromptReply {
21
21
  readonly resolve: (value: string) => void
@@ -25,6 +25,11 @@ interface PromptReply {
25
25
 
26
26
  export interface ProviderAuthorizationPanelProps {
27
27
  readonly row: ProviderAuthorizationRow
28
+ /**
29
+ * Start this method immediately instead of showing the picker: a
30
+ * single-method provider signs in with no extra step.
31
+ */
32
+ readonly autoStartMethod?: string
28
33
  begin(
29
34
  row: ProviderAuthorizationRow,
30
35
  method: string,
@@ -75,14 +80,31 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
75
80
  decline()
76
81
  }
77
82
 
83
+ const propsRef = useRef(props)
84
+ propsRef.current = props
85
+ const rowKey = props.row.key
86
+ // One-shot auto start (single-method providers): guarded by a ref so a
87
+ // remount of the same row cannot start a second attempt.
88
+ const autoStarted = useRef(false)
89
+ useEffect(() => {
90
+ const method = props.autoStartMethod
91
+ if (method === undefined || autoStarted.current) return
92
+ autoStarted.current = true
93
+ if (phase !== 'methods') return
94
+ if (props.row.methods.some(entry => entry.id === method)) start(method)
95
+ // One mount-time decision: the auto method, the method list, and the
96
+ // phase are fixed for this mount, so re-running on their identity would
97
+ // risk starting a second attempt.
98
+ // eslint-disable-next-line react-hooks/exhaustive-deps
99
+ }, [props.autoStartMethod])
78
100
  useEffect(() => () => {
79
101
  controllerRef.current?.abort()
80
- props.cancel(props.row.key)
102
+ propsRef.current.cancel(rowKey)
81
103
  const reply = replyRef.current
82
104
  replyRef.current = undefined
83
105
  reply?.detach()
84
106
  reply?.reject(new AuthorizationDeclinedError())
85
- }, [props.row.key])
107
+ }, [rowKey])
86
108
 
87
109
  const start = (method: string): void => {
88
110
  setPhase('running')
@@ -0,0 +1,79 @@
1
+ /** Bounded slash-command and mention completion menu. */
2
+
3
+ import { createElement, type ReactElement } from 'react'
4
+ import { Box, Text, useStdout } from 'ink'
5
+ import type { CompletionCandidate } from '../completion.ts'
6
+ import { selectionWindow } from '../render/inspector.ts'
7
+ import { visibleColumns } from '../render/markdown.ts'
8
+ import { displayText, padColumns, singleLineText, truncateColumns } from '../render/text.ts'
9
+ import { dim, getPalette, inkColor } from '../theme.ts'
10
+
11
+ /** Shared geometry used by both the menu view and the App dynamic-row budget. */
12
+ function completionMenuMetrics(terminalRows: number): { limit: number; showFooter: boolean; verticalPadding: number } {
13
+ const showFooter = terminalRows >= 12
14
+ const verticalPadding = terminalRows >= 14 ? 1 : 0
15
+ const limit = Math.max(1, Math.min(6, terminalRows - (showFooter ? 11 : 10) - verticalPadding * 2))
16
+ return { limit, showFooter, verticalPadding }
17
+ }
18
+
19
+ /** Total physical rows occupied by the completion menu at this terminal height. */
20
+ export function completionMenuRowCount(terminalRows: number, rowCount: number): number {
21
+ const { limit, showFooter, verticalPadding } = completionMenuMetrics(terminalRows)
22
+ const visible = rowCount === 0 ? 1 : Math.min(rowCount, limit)
23
+ const hidden = rowCount === 0 ? 0 : rowCount - visible
24
+ return visible + (hidden > 0 ? 1 : 0) + (showFooter ? 1 : 0) + verticalPadding * 2
25
+ }
26
+
27
+ /** Completion view anchored directly above the composer band. */
28
+ export function CompletionMenu({ active, mention, index, rows, error }: {
29
+ active: boolean
30
+ mention: boolean
31
+ index: number
32
+ rows: readonly CompletionCandidate[]
33
+ error?: string
34
+ }): ReactElement | undefined {
35
+ const stdout = useStdout().stdout
36
+ const columns = stdout?.columns ?? 80
37
+ const terminalRows = stdout?.rows ?? 30
38
+ if (!active) return undefined
39
+ const contentColumns = Math.max(1, columns - 4)
40
+ const nameWidth = mention
41
+ ? Math.max(1, contentColumns - 2)
42
+ : Math.min(18, Math.max(1, contentColumns - 2), Math.max(0, ...rows.map(row => visibleColumns(row.label))) + 2)
43
+ const descBudget = Math.max(0, contentColumns - nameWidth - 2)
44
+ const { limit, showFooter, verticalPadding } = completionMenuMetrics(terminalRows)
45
+ const selected = rows.length === 0 ? 0 : index % rows.length
46
+ const first = selectionWindow(selected, rows.length, limit)
47
+ const visible = rows.slice(first, first + limit)
48
+ const hidden = rows.length - visible.length
49
+ return createElement(
50
+ Box,
51
+ { flexDirection: 'column', marginLeft: 2, paddingY: verticalPadding },
52
+ ...(rows.length === 0
53
+ ? [error === undefined
54
+ ? createElement(Text, { key: 'loading', dimColor: true }, 'searching…')
55
+ : createElement(
56
+ Text,
57
+ { key: 'error', color: inkColor(getPalette().error), wrap: 'truncate-end' },
58
+ truncateColumns(`workspace search unavailable: ${singleLineText(error)} · keep typing to retry`, contentColumns),
59
+ )]
60
+ : visible.map((candidate, at) => {
61
+ const absolute = first + at
62
+ return createElement(
63
+ Text,
64
+ {
65
+ key: candidate.label,
66
+ color: absolute === selected ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
67
+ wrap: 'truncate-end',
68
+ },
69
+ `${absolute === selected ? '❯ ' : ' '}${padColumns(candidate.label, nameWidth)}${dim(truncateColumns(displayText(candidate.description), descBudget))}`,
70
+ )
71
+ })),
72
+ hidden > 0
73
+ ? createElement(Text, { key: 'more', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, ` … +${hidden} more`)
74
+ : undefined,
75
+ showFooter
76
+ ? createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, dim(mention ? `↑↓ choose · ${rows.length} items · tab insert` : `↑↓ choose · ${rows.length} items · tab complete`))
77
+ : undefined,
78
+ )
79
+ }