dsh-ssh-tui 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -9,6 +9,10 @@ If you mostly work over SSH — a jump host, a test box, a keyboard-only
9
9
  session — start here. A local desktop terminal with themes and layout
10
10
  you already like can stay as it is.
11
11
 
12
+ Listed on the [dshfind plugin directory](https://dshfind.com/en/plugins/cyjyyd/dsh-ssh-tui):
13
+
14
+ [![dshfind](https://dshfind.com/api/card/cyjyyd/dsh-ssh-tui?lang=en)](https://dshfind.com/en/plugins/cyjyyd/dsh-ssh-tui?ref=badge)
15
+
12
16
  Install with the official CLI (no clone):
13
17
 
14
18
  ```sh
@@ -356,7 +360,7 @@ collapsed card instead of being prefixed onto parent transcript lines;
356
360
  `/subagents` lists active runs.
357
361
 
358
362
  `/mode` opens the agent-mode picker backed by dsh's official preset roster:
359
- 标准模式 (standard), PTC 模式 (code), 极简模式 (minimal), 创造模式 (cordis),
363
+ 标准模式 (standard), PTC 模式 (ptc; `code` on dsh 0.1.1), 极简模式 (minimal), 创造模式 (cordis),
360
364
  智能路由模式 (routing-suite, from `dsh-routing-suite`),
361
365
  plus any locally authored presets (e.g. `whoami-standard`). On a session that
362
366
  has not produced work the switch applies immediately; otherwise it is remembered
package/README.md CHANGED
@@ -10,6 +10,10 @@ English: [README.en.md](README.en.md)
10
10
 
11
11
  SuperGrok / X Premium 订阅走配套插件 [dsh-llm-xai-oauth](https://github.com/cyjyyd/dsh-llm-xai-oauth),复用本机 grok-bridge token,不需要 xAI API Key。
12
12
 
13
+ 本插件已被 [dshfind 插件目录](https://dshfind.com/zh/plugins/cyjyyd/dsh-ssh-tui) 收录:
14
+
15
+ [![dshfind](https://dshfind.com/api/card/cyjyyd/dsh-ssh-tui?lang=zh)](https://dshfind.com/zh/plugins/cyjyyd/dsh-ssh-tui?ref=badge)
16
+
13
17
  安装(官方 CLI,无需 clone):
14
18
 
15
19
  ```bash
@@ -194,6 +198,8 @@ dsh --profile tui --no-color
194
198
  | `Ctrl+D` | 退出 |
195
199
  | `Ctrl+L` | 重绘整个画面 |
196
200
 
201
+ `/mode` 切换官方 preset:标准 (`standard`)、PTC (`ptc`;dsh 0.1.1 上仍是 `code`)、极简 (`minimal`)、创造 (`cordis`),以及本地安装的其它模式。
202
+
197
203
  斜杠命令:`/help`、`/find`、`/model`、`/provider`、`/submodel`、`/subeffort`、`/mode`、`/resume`、
198
204
  `/status`、`/subagents`、`/usage`(`/balance` 同义)、`/setup`、`/clear`,
199
205
  以及 harness 自带命令(`/goal`、`/plan`、`/compact` 等)。`/compact` 进行中会显示
package/cordis.patch.yml CHANGED
@@ -13,14 +13,19 @@
13
13
  directory is {{cwd}}. Verify your work by running the code or tests.
14
14
  Keep answers brief and factual.
15
15
 
16
- # The shared module-reload HMR row stays off; the launcher's watch-only
17
- # fallback keeps user patch layers live until the TUI exits.
16
+ # A patch replaces the targeted row's whole `config`, so restating `root`
17
+ # keeps the official HMR row complete while the TUI keeps module reload off.
18
+ # The launcher's watch-only fallback still keeps user patch layers live.
18
19
  - id: hmr
19
20
  disabled: true
21
+ config:
22
+ root: ['.']
20
23
 
21
24
  - id: tools
22
25
  config:
23
- mode: !!js process.env.DSH_TOOLS_MODE
26
+ # 0.1.2 renamed the tools-mode value `code` → `ptc` with no alias.
27
+ # Map either env value onto the schema this dsh install actually accepts.
28
+ mode: !!js (function () { var mode = process.env.DSH_TOOLS_MODE; if (mode !== 'code' && mode !== 'ptc') return mode; try { var req = process.getBuiltinModule('module').createRequire(ctx.baseUrl || process.cwd() + '/'); var ver = req('@deepseek-ai/dsh-tools/package.json').version || ''; if (!/^0\.(0|1)\.(0|1)/.test(ver)) return 'ptc'; return 'code' } catch (err) { if (mode === 'code') return 'ptc'; return mode } })()
24
29
 
25
30
  # The `main` agent is created by the TUI plugin itself after the loader
26
31
  # settles, so it can read the saved default provider/model from
@@ -63,6 +68,15 @@
63
68
  config:
64
69
  default: standard
65
70
 
71
+ # Host-plane PTC runtime used by the shipped `ptc` preset. Absent on
72
+ # dsh-base; web/headless insert it themselves. Keep it optional so an
73
+ # older dsh install without the package still boots standard mode.
74
+ # Disabled when the package is not installed (older dsh 0.1.1 runtimes
75
+ # still boot; 0.1.2's ptc preset can mount once the worker is on disk).
76
+ - id: code-runtime
77
+ name: '@deepseek-ai/dsh-code-runtime-worker-thread'
78
+ disabled: !!js (function () { try { process.getBuiltinModule('module').createRequire(ctx.baseUrl || process.cwd() + '/').resolve('@deepseek-ai/dsh-code-runtime-worker-thread'); return false } catch (err) { return true } })()
79
+
66
80
  - id: ssh-tui-startup
67
81
  name: 'dsh-ssh-tui/startup'
68
82
 
package/lib/tui.js CHANGED
@@ -26,8 +26,27 @@ import { defaultReasoningEffort } from './reasoning.js';
26
26
  import { checkForPluginUpdate } from './update-check.js';
27
27
  import { ROUTE_MEMORY_NAMESPACE, parseRouteMemory, rememberedRouteFor, upsertRememberedRoute, } from './route-memory.js';
28
28
  import { DEFAULT_SUBAGENT_MODEL, SUBAGENT_SETTINGS_NAMESPACE, defaultSubagentModelForProvider, subagentModelMatchesProvider, subagentSettingsValue, } from './subagent-model.js';
29
- const ROUTE_MEMORY_NS = ROUTE_MEMORY_NAMESPACE;
30
29
  import { UserQuestionError, } from '@deepseek-ai/dsh-user-questions';
30
+ function discoverProviderModels(llm, request, signal) {
31
+ return llm.discoverModels(settingsNamespace('llm-pi-ai'), { ...request, signal }, signal);
32
+ }
33
+ function installUserQuestionAnswerer(ctx, questions, ask) {
34
+ const provider = questions;
35
+ if (typeof provider.registerProvider === 'function') {
36
+ return provider.registerProvider({ ask });
37
+ }
38
+ return ctx.on('user-questions/request', async (request, next) => {
39
+ try {
40
+ return await ask(request);
41
+ }
42
+ catch (error) {
43
+ if (error instanceof UserQuestionError && error.code === 'ASK_ABORTED')
44
+ throw error;
45
+ return await next();
46
+ }
47
+ });
48
+ }
49
+ const ROUTE_MEMORY_NS = ROUTE_MEMORY_NAMESPACE;
31
50
  const PROVIDER_TEMPLATES = {
32
51
  official: {
33
52
  label: 'DeepSeek 官方',
@@ -492,7 +511,7 @@ const LOCAL_COMMANDS = [
492
511
  { name: 'provider', description: 'switch provider, then model and reasoning effort' },
493
512
  { name: 'submodel', description: `select subagent model (default ${DEFAULT_SUBAGENT_MODEL}, same provider as parent)` },
494
513
  { name: 'subeffort', description: 'select subagent reasoning effort (default follows provider)' },
495
- { name: 'mode', description: 'switch agent mode / preset (standard, minimal, code, cordis, routing-suite, ...)' },
514
+ { name: 'mode', description: 'switch agent mode / preset (standard, minimal, ptc, cordis, routing-suite, ...)' },
496
515
  { name: 'quit', description: 'exit the TUI' },
497
516
  { name: 'exit', description: 'exit the TUI' },
498
517
  { name: 'clear', description: 'clear the transcript view' },
@@ -1197,9 +1216,14 @@ export function remainingPercentFromUsed(usedPercent) {
1197
1216
  return 100;
1198
1217
  return Math.max(0, Math.min(100, Math.round((100 - usedPercent) * 10) / 10));
1199
1218
  }
1200
- /** Cross a remaining-percent threshold from above (50 / 25 / 10 / 5). */
1219
+ /** Cross a remaining-percent threshold from above (50 / 25 / 10 / 5).
1220
+ * Only the tightest (lowest) crossed threshold is returned, so one drop
1221
+ * never paints 50/25/10 as three identical warnings. */
1201
1222
  export function crossedQuotaThresholds(previousRemaining, remaining) {
1202
- return QUOTA_ALERT_THRESHOLDS.filter(threshold => remaining <= threshold && (previousRemaining === undefined || previousRemaining > threshold));
1223
+ const crossed = QUOTA_ALERT_THRESHOLDS.filter(threshold => remaining <= threshold && (previousRemaining === undefined || previousRemaining > threshold));
1224
+ if (crossed.length === 0)
1225
+ return [];
1226
+ return [crossed[crossed.length - 1]];
1203
1227
  }
1204
1228
  export function quotaAlertText(snapshot, window) {
1205
1229
  const reset = window.resetsAt === undefined ? '' : `(${formatQuotaReset(window.resetsAt)})`;
@@ -1207,11 +1231,13 @@ export function quotaAlertText(snapshot, window) {
1207
1231
  }
1208
1232
  /**
1209
1233
  * How often to re-fetch quota, based on the tightest window.
1210
- * Hourly/5h: every 10 turns, every 4 when near a threshold.
1211
- * Weekly: every 50 turns, every 10 when near.
1212
- * Monthly: every 80 turns, every 20 when near.
1234
+ * Counted in model steps (not conversation turns): a turn with several
1235
+ * tool/LLM steps should refresh more often because it spends more quota.
1236
+ * Hourly/5h: every 10 steps, every 4 when near a threshold.
1237
+ * Weekly: every 50 steps, every 10 when near.
1238
+ * Monthly: every 80 steps, every 20 when near.
1213
1239
  */
1214
- export function quotaRefreshEveryTurns(window) {
1240
+ export function quotaRefreshEverySteps(window) {
1215
1241
  if (window === undefined)
1216
1242
  return 10;
1217
1243
  const near = window.remainingPercent <= QUOTA_NEAR_THRESHOLD_PERCENT;
@@ -1223,6 +1249,8 @@ export function quotaRefreshEveryTurns(window) {
1223
1249
  return near ? 20 : 80;
1224
1250
  return near ? 10 : 50;
1225
1251
  }
1252
+ /** @deprecated Same cadence as {@link quotaRefreshEverySteps}; the name predates step accounting. */
1253
+ export const quotaRefreshEveryTurns = quotaRefreshEverySteps;
1226
1254
  function quotaPeriodLabel(period) {
1227
1255
  if (period === 'hourly')
1228
1256
  return '5 小时';
@@ -2353,6 +2381,7 @@ export class SshTui {
2353
2381
  lastPaintWidth = 0;
2354
2382
  lastPaintHeight = 0;
2355
2383
  lastChromeStart = 0;
2384
+ lastTranscriptStart = -1;
2356
2385
  paintIntervalMs;
2357
2386
  paintLink = 'local';
2358
2387
  paintProbed = false;
@@ -2361,7 +2390,7 @@ export class SshTui {
2361
2390
  llmRetry;
2362
2391
  quotaSnapshot;
2363
2392
  quotaAlerted = new Set();
2364
- quotaTurnsSinceRefresh = 0;
2393
+ quotaStepsSinceRefresh = 0;
2365
2394
  quotaRefreshInFlight = false;
2366
2395
  searchHits = [];
2367
2396
  searchIndex = -1;
@@ -2405,7 +2434,7 @@ export class SshTui {
2405
2434
  this.disposers.push(this.ctx.on('session/event', this.handleSessionEvent), this.ctx.on('agent/status', this.handleStatus), this.ctx.on('agent/error', this.handleError), this.ctx.on('agent/disposed', this.handleDisposed), this.ctx.on('agent/inbox/claimed', this.handleInboxClaimed), this.ctx.on('agent/inbox/discarded', this.handleInboxDiscarded), this.ctx.on('agent/request', this.handleAgentRequest), this.ctx.on('subagent/start', this.handleSubagentStart), this.ctx.on('subagent/end', this.handleSubagentEnd), this.ctx.on('approval/request', this.handleApproval));
2406
2435
  const questions = this.ctx.get('userQuestions');
2407
2436
  if (questions !== undefined) {
2408
- this.userQuestionDisposer = questions.registerProvider({ ask: this.handleUserQuestions });
2437
+ this.userQuestionDisposer = installUserQuestionAnswerer(this.ctx, questions, this.handleUserQuestions);
2409
2438
  }
2410
2439
  this.write(`${this.useAlternateScreen ? '\x1b[?1049h' : ''}\x1b[?1000h\x1b[?1006h\x1b[?2004h\x1b[?25l`);
2411
2440
  this.render();
@@ -3598,7 +3627,8 @@ export class SshTui {
3598
3627
  String(chromeStart),
3599
3628
  ].join('\x1f');
3600
3629
  const chromeChanged = chromeKey !== this.lastChromeKey || chromeStart !== this.lastChromeStart;
3601
- const sizeChanged = width !== this.lastPaintWidth || height !== this.lastPaintHeight;
3630
+ const transcriptScrolled = start !== this.lastTranscriptStart;
3631
+ const sizeChanged = width !== this.lastPaintWidth || height !== this.lastPaintHeight || transcriptScrolled;
3602
3632
  // One stdout write per frame: dirty rows only, so jump-host SSH sees a
3603
3633
  // single packet instead of one write per line. Clip/pad so leftover
3604
3634
  // wide glyphs cannot wrap into the input box.
@@ -3621,6 +3651,7 @@ export class SshTui {
3621
3651
  this.lastPaintWidth = width;
3622
3652
  this.lastPaintHeight = height;
3623
3653
  this.lastChromeStart = chromeStart;
3654
+ this.lastTranscriptStart = start;
3624
3655
  };
3625
3656
  buildSuggestions() {
3626
3657
  const input = this.input;
@@ -3769,8 +3800,9 @@ export class SshTui {
3769
3800
  kind === 'reasoning' ? '2;3' :
3770
3801
  kind === 'brand' ? '1;38;2;77;107;253' :
3771
3802
  kind === 'tool' || kind === 'tool-result' ? '33' :
3772
- kind === 'diff-add' ? '38;5;22;48;5;194' :
3773
- kind === 'diff-del' ? '38;5;124;48;5;224' :
3803
+ // Codex-like: muted add/del that blend into the terminal background.
3804
+ kind === 'diff-add' ? '38;2;122;168;116;48;2;18;42;24' :
3805
+ kind === 'diff-del' ? '38;2;196;122;122;48;2;48;20;20' :
3774
3806
  kind === 'diff-path' ? '1;36' :
3775
3807
  kind === 'todo-done' ? '2;32' :
3776
3808
  kind === 'todo-active' ? '1;36' :
@@ -4006,6 +4038,16 @@ export class SshTui {
4006
4038
  // Usage accounting is complete for this step; the map only exists to
4007
4039
  // deduplicate repeated usage reports during the step.
4008
4040
  this.usageByStep.delete(`${event.data.turn}:${event.data.step}`);
4041
+ if (!this.replaying) {
4042
+ this.quotaStepsSinceRefresh += 1;
4043
+ const every = quotaRefreshEverySteps(this.quotaSnapshot === undefined
4044
+ ? undefined
4045
+ : tightestQuotaWindow(this.quotaSnapshot));
4046
+ if (this.quotaStepsSinceRefresh >= every) {
4047
+ this.quotaStepsSinceRefresh = 0;
4048
+ void this.refreshQuota({ reason: 'step', announce: false }).catch(() => { });
4049
+ }
4050
+ }
4009
4051
  this.markDirty();
4010
4052
  break;
4011
4053
  }
@@ -4016,14 +4058,6 @@ export class SshTui {
4016
4058
  this.markDirty();
4017
4059
  break;
4018
4060
  case 'turn/end': {
4019
- this.quotaTurnsSinceRefresh += 1;
4020
- const every = quotaRefreshEveryTurns(this.quotaSnapshot === undefined
4021
- ? undefined
4022
- : tightestQuotaWindow(this.quotaSnapshot));
4023
- if (this.quotaTurnsSinceRefresh >= every) {
4024
- this.quotaTurnsSinceRefresh = 0;
4025
- void this.refreshQuota({ reason: 'turn', announce: false }).catch(() => { });
4026
- }
4027
4061
  const reason = event.data.reason;
4028
4062
  this.openToolCalls.clear();
4029
4063
  this.pendingToolTimes.clear();
@@ -4062,11 +4096,6 @@ export class SshTui {
4062
4096
  this.markDirty();
4063
4097
  break;
4064
4098
  }
4065
- case 'todo/write': {
4066
- this.upsertPlanRow({ todos: parsePlanTodos(event.data.todos) });
4067
- this.markDirty();
4068
- break;
4069
- }
4070
4099
  default:
4071
4100
  this.handleExtensionEvent(event);
4072
4101
  break;
@@ -4132,6 +4161,11 @@ export class SshTui {
4132
4161
  this.markDirty();
4133
4162
  return;
4134
4163
  }
4164
+ if (type === 'todo/write') {
4165
+ this.upsertPlanRow({ todos: parsePlanTodos(data?.todos) });
4166
+ this.markDirty();
4167
+ return;
4168
+ }
4135
4169
  if (type === 'command/run') {
4136
4170
  this.handleCommandRun(data);
4137
4171
  return;
@@ -4707,12 +4741,11 @@ export class SshTui {
4707
4741
  const llm = this.ctx.get('llm');
4708
4742
  if (llm === undefined)
4709
4743
  return [];
4710
- const discovered = await llm.discoverModels(settingsNamespace('llm-pi-ai'), {
4744
+ const discovered = await discoverProviderModels(llm, {
4711
4745
  baseURL,
4712
4746
  ...(api === undefined ? {} : { api }),
4713
4747
  ...(apiKey === undefined ? {} : { apiKey }),
4714
- signal: AbortSignal.timeout(15_000),
4715
- });
4748
+ }, AbortSignal.timeout(15_000));
4716
4749
  return discovered.map(model => ({ id: model.id, label: model.name || model.id }));
4717
4750
  }
4718
4751
  /** Add one endpoint-listed model to the stored provider profile when needed. */
@@ -5112,7 +5145,7 @@ export class SshTui {
5112
5145
  return;
5113
5146
  this.quotaSnapshot = undefined;
5114
5147
  this.quotaAlerted.clear();
5115
- this.quotaTurnsSinceRefresh = 0;
5148
+ this.quotaStepsSinceRefresh = 0;
5116
5149
  this.markDirty();
5117
5150
  }
5118
5151
  /** Persist one subagent selection and publish it to the live request waterfall. */
@@ -5254,7 +5287,7 @@ export class SshTui {
5254
5287
  });
5255
5288
  this.markDirty();
5256
5289
  }
5257
- /** /mode: pick an agent preset (standard / minimal / code / cordis / routing-suite / ...). */
5290
+ /** /mode: pick an agent preset (standard / minimal / ptc / cordis / routing-suite / ...). */
5258
5291
  async runModeCommand() {
5259
5292
  const agentPresets = this.ctx.get('agentPresets');
5260
5293
  if (agentPresets === undefined) {
@@ -6149,12 +6182,11 @@ export class SshTui {
6149
6182
  const llm = this.ctx.get('llm');
6150
6183
  if (llm === undefined)
6151
6184
  throw new Error('llm 服务不可用');
6152
- const discovered = await llm.discoverModels(settingsNamespace('llm-pi-ai'), {
6185
+ const discovered = await discoverProviderModels(llm, {
6153
6186
  baseURL,
6154
6187
  ...(template.api === undefined ? {} : { api: template.api }),
6155
6188
  ...(key === '' ? {} : { apiKey: key }),
6156
- signal: AbortSignal.timeout(15_000),
6157
- });
6189
+ }, AbortSignal.timeout(15_000));
6158
6190
  // Apply only if the wizard is still on the same draft the fetch started
6159
6191
  // from, so a stale reply cannot overwrite a newer edit or a reset.
6160
6192
  const stillCurrent = this.onboarding === state