@hmharness/cli 0.4.2 → 0.4.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.
package/dist/tui.d.ts CHANGED
@@ -46,6 +46,8 @@ export declare class TuiRuntime {
46
46
  private modeTag;
47
47
  /** rows for the `/model ` picker (configured providers first, set by driver) */
48
48
  private modelChoices;
49
+ /** rows for the `/resume ` session picker (id prefix + first user line) */
50
+ private sessionChoices;
49
51
  /** Wheel/click handling: NO mouse reporting by default - select/copy
50
52
  * always works and terminals translate the wheel to arrow keys on the
51
53
  * alternate screen. Reporting turns on ONLY while a palette is open
@@ -63,9 +65,16 @@ export declare class TuiRuntime {
63
65
  name: string;
64
66
  desc: string;
65
67
  }>): void;
68
+ setSessionChoices(list: Array<{
69
+ name: string;
70
+ desc: string;
71
+ }>): void;
66
72
  /** Focus the /model picker (used by the bare `/model` command so the
67
73
  * printed list is never a dead end - the live palette opens on it). */
68
74
  openModelPicker(): void;
75
+ /** Focus the /resume session picker - same live palette as /model:
76
+ * arrows/wheel navigate, Enter loads the highlighted session. */
77
+ openSessionPicker(): void;
69
78
  /** Run the highlighted palette row (shared by Enter and palette clicks);
70
79
  * with no palette open it just submits the typed input. */
71
80
  private pickHighlighted;
@@ -89,8 +98,9 @@ export declare class TuiRuntime {
89
98
  * while it shows, clicks choose its rows and the wheel drives its
90
99
  * selection; drag-select resumes the moment it closes. */
91
100
  private syncMouseReporting;
92
- /** The palette data source: `/model ` opens the model picker, otherwise
93
- * slash commands. Rows are {name, desc} so both share one renderer. */
101
+ /** The palette data source: `/model ` opens the model picker, `/resume `
102
+ * the session picker, otherwise slash commands. Rows are {name, desc} so
103
+ * all three share one renderer, keyboard and click machinery. */
94
104
  private panelItems;
95
105
  configure(model: string, cwdName: string, skillCount: number, locale: Locale, version?: string): void;
96
106
  destroy(): void;
package/dist/tui.js CHANGED
@@ -167,6 +167,8 @@ export class TuiRuntime {
167
167
  modeTag = '';
168
168
  /** rows for the `/model ` picker (configured providers first, set by driver) */
169
169
  modelChoices = [];
170
+ /** rows for the `/resume ` session picker (id prefix + first user line) */
171
+ sessionChoices = [];
170
172
  /** Wheel/click handling: NO mouse reporting by default - select/copy
171
173
  * always works and terminals translate the wheel to arrow keys on the
172
174
  * alternate screen. Reporting turns on ONLY while a palette is open
@@ -195,6 +197,10 @@ export class TuiRuntime {
195
197
  this.modelChoices = list;
196
198
  this.dirty = true;
197
199
  }
200
+ setSessionChoices(list) {
201
+ this.sessionChoices = list;
202
+ this.dirty = true;
203
+ }
198
204
  /** Focus the /model picker (used by the bare `/model` command so the
199
205
  * printed list is never a dead end - the live palette opens on it). */
200
206
  openModelPicker() {
@@ -203,13 +209,26 @@ export class TuiRuntime {
203
209
  this.cmdIdx = 0;
204
210
  this.dirty = true;
205
211
  }
212
+ /** Focus the /resume session picker - same live palette as /model:
213
+ * arrows/wheel navigate, Enter loads the highlighted session. */
214
+ openSessionPicker() {
215
+ this.input = '/resume ';
216
+ this.caret = this.input.length;
217
+ this.cmdIdx = 0;
218
+ this.dirty = true;
219
+ }
206
220
  /** Run the highlighted palette row (shared by Enter and palette clicks);
207
221
  * with no palette open it just submits the typed input. */
208
222
  pickHighlighted() {
209
223
  const hits = this.panelItems(this.input);
210
224
  const pick = hits.length ? hits[Math.min(this.cmdIdx, hits.length - 1)].name : '';
211
225
  if (pick) {
212
- this.input = this.input.startsWith('/model') ? `/model ${pick} ` : pick + ' ';
226
+ if (this.input.startsWith('/model'))
227
+ this.input = `/model ${pick} `;
228
+ else if (this.input.startsWith('/resume'))
229
+ this.input = `/resume ${pick}`;
230
+ else
231
+ this.input = pick + ' ';
213
232
  this.caret = this.input.length;
214
233
  this.cmdIdx = 0;
215
234
  }
@@ -271,8 +290,9 @@ export class TuiRuntime {
271
290
  this.mouseReported = want;
272
291
  stdout.write(want ? '\x1b[?1000h\x1b[?1006h' : '\x1b[?1000l\x1b[?1006l');
273
292
  }
274
- /** The palette data source: `/model ` opens the model picker, otherwise
275
- * slash commands. Rows are {name, desc} so both share one renderer. */
293
+ /** The palette data source: `/model ` opens the model picker, `/resume `
294
+ * the session picker, otherwise slash commands. Rows are {name, desc} so
295
+ * all three share one renderer, keyboard and click machinery. */
276
296
  panelItems(input) {
277
297
  if (input === '/model' || input.startsWith('/model ')) {
278
298
  const q = input.slice(6).trim().toLowerCase();
@@ -283,6 +303,10 @@ export class TuiRuntime {
283
303
  const all = [...configured, ...rest];
284
304
  return q ? all.filter((i) => i.name.toLowerCase().startsWith(q)) : all;
285
305
  }
306
+ if (input === '/resume' || input.startsWith('/resume ')) {
307
+ const q = input.slice(7).trim().toLowerCase();
308
+ return q ? this.sessionChoices.filter((i) => i.name.toLowerCase().startsWith(q)) : this.sessionChoices;
309
+ }
286
310
  return matchCommands(input).map((c) => ({ name: c.name, desc: String(this.t[c.key]) }));
287
311
  }
288
312
  configure(model, cwdName, skillCount, locale, version) {
@@ -509,6 +533,12 @@ export class TuiRuntime {
509
533
  this.openModelPicker();
510
534
  return;
511
535
  }
536
+ // same two-stage rule for /resume: bare command + Enter opens the
537
+ // session picker (focus moves to the list), never loads row 0 blindly
538
+ if (this.input === '/resume') {
539
+ this.openSessionPicker();
540
+ return;
541
+ }
512
542
  // palette open: Enter runs the highlighted row (a command, or a
513
543
  // /model target), not the raw input; without a palette it submits
514
544
  this.pickHighlighted();
@@ -519,7 +549,9 @@ export class TuiRuntime {
519
549
  if (hits.length) {
520
550
  this.input = this.input.startsWith('/model')
521
551
  ? `/model ${hits[Math.min(this.cmdIdx, hits.length - 1)].name} `
522
- : hits[Math.min(this.cmdIdx, hits.length - 1)].name + ' ';
552
+ : this.input.startsWith('/resume')
553
+ ? `/resume ${hits[Math.min(this.cmdIdx, hits.length - 1)].name}`
554
+ : hits[Math.min(this.cmdIdx, hits.length - 1)].name + ' ';
523
555
  this.caret = this.input.length;
524
556
  this.cmdIdx = 0;
525
557
  this.dirty = true;
@@ -925,29 +957,34 @@ export async function tui(yes, noWeb = false) {
925
957
  if (line === '/resume' || line.startsWith('/resume ')) {
926
958
  const arg = line.slice(8).trim();
927
959
  const { latestSession, loadTranscript } = await import('@hmharness/kernel');
928
- if (!arg) {
929
- // list the 8 newest sessions: id prefix (enough to disambiguate) + first user line
930
- const { readdir } = await import('node:fs/promises');
931
- let files = [];
960
+ const { readdir } = await import('node:fs/promises');
961
+ // refresh the picker rows: id prefix + first user line as the preview
962
+ let files = [];
963
+ try {
964
+ files = (await readdir(join(home, 'sessions'))).filter((f) => f.endsWith('.jsonl'));
965
+ }
966
+ catch { /* none */ }
967
+ files.sort();
968
+ const recent = files.slice(-8).reverse();
969
+ const rows = [];
970
+ for (const f of recent) {
932
971
  try {
933
- files = (await readdir(join(home, 'sessions'))).filter((f) => f.endsWith('.jsonl'));
972
+ const tr = await loadTranscript(join(home, 'sessions', f));
973
+ const firstUser = tr?.messages.find((m) => m.role === 'user')?.content ?? '';
974
+ rows.push({ name: f.slice(0, 18), desc: (firstUser || '(no user line)').replace(/\n/g, ' ').slice(0, 56) });
934
975
  }
935
- catch { /* none */ }
936
- files.sort();
937
- const recent = files.slice(-8).reverse();
938
- if (recent.length === 0) {
976
+ catch { /* skip unreadable */ }
977
+ }
978
+ rt.setSessionChoices(rows);
979
+ if (!arg) {
980
+ // bare /resume opens the LIVE picker - same arrows/wheel/Enter/click
981
+ // machinery as /model; a printed text list is a dead end (user-
982
+ // reported: "上下键无法选择")
983
+ if (rows.length === 0) {
939
984
  rt.addText(t.cmdResumeNone, 'dim');
940
985
  return;
941
986
  }
942
- for (const f of recent) {
943
- try {
944
- const tr = await loadTranscript(join(home, 'sessions', f));
945
- const firstUser = tr?.messages.find((m) => m.role === 'user')?.content ?? '';
946
- rt.addText(`${CYAN(f.slice(0, 18))} ${(firstUser || '(no user line)').replace(/\n/g, ' ').slice(0, 60)}`, 'dim');
947
- }
948
- catch { /* skip unreadable */ }
949
- }
950
- rt.addText(t.cmdResumeHint, 'dim');
987
+ rt.openSessionPicker();
951
988
  return;
952
989
  }
953
990
  const file = await latestSession(home, arg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "hmharness command line: one-shot tasks, an interactive REPL, a fullscreen TUI, the web frontend, and direct tool invocation.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -43,11 +43,11 @@
43
43
  "build": "tsc -p tsconfig.build.json"
44
44
  },
45
45
  "dependencies": {
46
- "@hmharness/kernel": "0.4.2",
47
- "@hmharness/evolution": "0.4.2",
48
- "@hmharness/domain-harmony": "0.4.2",
49
- "@hmharness/domain-ops": "0.4.2",
50
- "@hmharness/agent": "0.4.2",
51
- "@hmharness/web": "0.4.2"
46
+ "@hmharness/kernel": "0.4.3",
47
+ "@hmharness/evolution": "0.4.3",
48
+ "@hmharness/domain-harmony": "0.4.3",
49
+ "@hmharness/domain-ops": "0.4.3",
50
+ "@hmharness/agent": "0.4.3",
51
+ "@hmharness/web": "0.4.3"
52
52
  }
53
53
  }