@hmharness/cli 0.6.4 → 0.6.6

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
@@ -32,6 +32,9 @@ export declare class TuiRuntime {
32
32
  private approval;
33
33
  private approvalResolve;
34
34
  private running;
35
+ /** queued task count: the busy hints line shows it live (queue at a glance,
36
+ * no /queue query needed) */
37
+ private queued;
35
38
  private renderTimer?;
36
39
  private exitResolve;
37
40
  private driver;
@@ -116,6 +119,7 @@ export declare class TuiRuntime {
116
119
  foldThinking(): void;
117
120
  setBusy(busy: boolean, label?: string): void;
118
121
  setStatus(s: string): void;
122
+ setQueued(n: number): void;
119
123
  requestApproval(name: string, args: Record<string, unknown>): Promise<boolean>;
120
124
  consumeInput(): string;
121
125
  onSubmit(fn: () => void): void;
package/dist/tui.js CHANGED
@@ -154,6 +154,9 @@ export class TuiRuntime {
154
154
  approval = null;
155
155
  approvalResolve = null;
156
156
  running = true;
157
+ /** queued task count: the busy hints line shows it live (queue at a glance,
158
+ * no /queue query needed) */
159
+ queued = 0;
157
160
  renderTimer;
158
161
  exitResolve = null;
159
162
  driver = null;
@@ -418,6 +421,10 @@ export class TuiRuntime {
418
421
  this.status = s;
419
422
  this.dirty = true;
420
423
  }
424
+ setQueued(n) {
425
+ this.queued = Math.max(0, n);
426
+ this.dirty = true;
427
+ }
421
428
  requestApproval(name, args) {
422
429
  this.approval = { name, args };
423
430
  this.dirty = true;
@@ -855,9 +862,11 @@ export class TuiRuntime {
855
862
  }
856
863
  }
857
864
  frame.push(DIM('└' + '─'.repeat(iw + 2) + '┘'));
858
- const hints = this.scrollFromBottom > 0
859
- ? `${DIM(this.t.tuiScrolled)}`
860
- : `${DIM(this.t.tuiHints)}`;
865
+ const hints = this.busy
866
+ ? `${DIM(this.t.tuiBusyHints(this.queued))}`
867
+ : this.scrollFromBottom > 0
868
+ ? `${DIM(this.t.tuiScrolled)}`
869
+ : `${DIM(this.t.tuiHints)}`;
861
870
  const stat = this.status && !this.busy ? DIM(this.status) : '';
862
871
  frame.push(truncateTo(hints + ' '.repeat(Math.max(1, W - strWidth(stripAnsi(hints)) - strWidth(stat))) + stat, W - 1));
863
872
  // Absolute per-row addressing: CUP resets the column and cancels the
@@ -935,34 +944,31 @@ export async function tui(yes, noWeb = false) {
935
944
  // Task queue: new submissions during a running task are queued (not
936
945
  // rejected, not run concurrently — sequential execution preserves history
937
946
  // integrity). Slash commands still run immediately (they're quick).
938
- // `!` prefix inserts at FRONT (urgent). Esc interrupts the running task.
947
+ // Codex-style interaction: ONE key does both jobs Enter sends when there
948
+ // is text and STOPS the running task when the line is empty (the TUI
949
+ // equivalent of the send button that becomes a stop button). No `!` prefix,
950
+ // no /queue skip: the queue is visible in the busy hints line.
939
951
  const taskQueue = [];
940
952
  let taskRunning = false;
941
953
  let currentAbort = null;
942
954
  rt.onSubmit(() => {
943
955
  const line = rt.consumeInput().trim();
944
- if (!line)
956
+ if (!line) {
957
+ // empty Enter while running = stop (interrupts the current task;
958
+ // queued tasks still run — clear them first with /queue clear if not)
959
+ if (taskRunning) {
960
+ currentAbort?.abort();
961
+ rt.addText('⏹ interrupting current task (in-flight tool calls finish first; queued tasks still run)', 'dim');
962
+ }
945
963
  return;
964
+ }
946
965
  if (line.startsWith('/')) {
947
966
  void handleLine(line);
948
967
  return;
949
968
  }
950
- if (line.startsWith('!')) {
951
- // urgent: strip the ! and insert at front of queue (or run immediately)
952
- const urgent = line.slice(1).trim();
953
- if (!urgent)
954
- return;
955
- if (taskRunning) {
956
- taskQueue.unshift(urgent);
957
- rt.addText(`⚡ inserted at front: "${urgent.slice(0, 60)}${urgent.length > 60 ? '…' : ''}" (runs next)`, 'dim');
958
- currentAbort?.abort(); // interrupt current to run the urgent task
959
- return;
960
- }
961
- void executeTaskQueue(urgent);
962
- return;
963
- }
964
969
  if (taskRunning) {
965
970
  taskQueue.push(line);
971
+ rt.setQueued(taskQueue.length);
966
972
  rt.addText(`📋 queued: "${line.slice(0, 60)}${line.length > 60 ? '…' : ''}" (${taskQueue.length} waiting)`, 'dim');
967
973
  return;
968
974
  }
@@ -974,6 +980,7 @@ export async function tui(yes, noWeb = false) {
974
980
  while (task) {
975
981
  await runSingleTask(task);
976
982
  task = taskQueue.shift();
983
+ rt.setQueued(taskQueue.length);
977
984
  if (task)
978
985
  rt.addText(`▶ next queued: "${task.slice(0, 60)}${task.length > 60 ? '…' : ''}"`, 'dim');
979
986
  }
@@ -1047,24 +1054,17 @@ export async function tui(yes, noWeb = false) {
1047
1054
  if (sub === 'clear') {
1048
1055
  const n = taskQueue.length;
1049
1056
  taskQueue.length = 0;
1057
+ rt.setQueued(0);
1050
1058
  rt.addText(n > 0 ? 'cleared ' + n + ' queued task(s)' : 'queue was already empty', 'dim');
1051
1059
  return;
1052
1060
  }
1053
- if (sub === 'skip' || sub === 'interrupt') {
1054
- if (!taskRunning) {
1055
- rt.addText('no task is running', 'dim');
1056
- return;
1057
- }
1058
- currentAbort?.abort();
1059
- rt.addText('interrupting current task (finishes in-flight tool calls)...', 'dim');
1060
- return;
1061
- }
1062
- // bare /queue: show status
1061
+ // bare /queue: show status. Operations are key-based, not command-based:
1062
+ // empty Enter stops the current task, typed input queues, this only inspects.
1063
1063
  const status = taskRunning ? 'running' : 'idle';
1064
1064
  const queueList = taskQueue.length > 0
1065
1065
  ? taskQueue.map((task, i) => ' ' + (i + 1) + '. ' + task.slice(0, 70)).join('\n')
1066
1066
  : ' (empty)';
1067
- rt.addText('queue: ' + status + ' | ' + taskQueue.length + ' waiting\n' + queueList + '\n\ncommands: /queue clear, /queue skip, !<task> to insert at front', 'dim');
1067
+ rt.addText('queue: ' + status + ' | ' + taskQueue.length + ' waiting\n' + queueList + '\n\nempty Enter = stop current · typed Enter = queue · /queue clear = drop all', 'dim');
1068
1068
  return;
1069
1069
  }
1070
1070
  if (line === '?' || line === '/help') {
@@ -1144,9 +1144,38 @@ export async function tui(yes, noWeb = false) {
1144
1144
  return;
1145
1145
  }
1146
1146
  history = tr.messages;
1147
- const firstUser = tr.messages.find((m) => m.role === 'user')?.content ?? '';
1148
1147
  rt.clearScreen();
1149
- rt.addUser(firstUser.replace(/\n/g, ' ').slice(0, 120));
1148
+ // Render the FULL session window, not just the first line (user
1149
+ // feedback: "恢复的应该是整个会话窗口,而不仅仅是片段"). Long sessions
1150
+ // are bounded from the tail so the visible window stays usable while
1151
+ // the complete transcript still lives in `history` for the model.
1152
+ const MAX_RENDER = 80;
1153
+ const msgs = tr.messages;
1154
+ const skipped = Math.max(0, msgs.length - MAX_RENDER);
1155
+ rt.addText('--- resumed ' + (tr.id || arg) + ' · ' + msgs.length + ' messages'
1156
+ + (skipped > 0 ? ' (' + skipped + ' earlier kept in context, not shown)' : '')
1157
+ + ' ---', 'dim');
1158
+ for (const m of (skipped > 0 ? msgs.slice(skipped) : msgs)) {
1159
+ const text = typeof m.content === 'string' ? m.content : '';
1160
+ if (m.role === 'user') {
1161
+ rt.addUser(text.replace(/\n+/g, ' ').slice(0, 400));
1162
+ }
1163
+ else if (m.role === 'assistant') {
1164
+ const calls = m.tool_calls ?? [];
1165
+ if (text.trim())
1166
+ rt.addText(text.slice(0, 2000));
1167
+ for (const c of calls)
1168
+ rt.addText('● ' + CYAN(String(c.function?.name ?? 'tool')) + DIM(' …'), 'dim');
1169
+ }
1170
+ else if (m.role === 'tool') {
1171
+ const first = text.split('\n').find((l) => l.trim()) ?? '';
1172
+ if (first)
1173
+ rt.addText(' ' + DIM('⎿ ' + first.trim().slice(0, 100)), 'dim');
1174
+ }
1175
+ else if (m.role === 'system' && text && !text.startsWith('[context pruned')) {
1176
+ rt.addText(DIM(text.slice(0, 300)), 'dim');
1177
+ }
1178
+ }
1150
1179
  rt.addText(t.cmdResumeLoaded(tr.messages.length), 'dim');
1151
1180
  return;
1152
1181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/cli",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
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.6.4",
47
- "@hmharness/evolution": "0.6.4",
48
- "@hmharness/domain-harmony": "0.6.4",
49
- "@hmharness/domain-ops": "0.6.4",
50
- "@hmharness/agent": "0.6.4",
51
- "@hmharness/web": "0.6.4"
46
+ "@hmharness/kernel": "0.6.6",
47
+ "@hmharness/evolution": "0.6.6",
48
+ "@hmharness/domain-harmony": "0.6.6",
49
+ "@hmharness/domain-ops": "0.6.6",
50
+ "@hmharness/agent": "0.6.6",
51
+ "@hmharness/web": "0.6.6"
52
52
  }
53
53
  }