@hmharness/cli 0.6.0 → 0.6.2

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 (2) hide show
  1. package/dist/tui.js +123 -52
  2. package/package.json +7 -7
package/dist/tui.js CHANGED
@@ -932,12 +932,109 @@ export async function tui(yes, noWeb = false) {
932
932
  void notifyUpdate(home, current, (latest) => rt.addText(`↑ ${t.updateHint(latest)}`, 'dim'));
933
933
  }
934
934
  let history = [];
935
+ // Task queue: new submissions during a running task are queued (not
936
+ // rejected, not run concurrently — sequential execution preserves history
937
+ // integrity). Slash commands still run immediately (they're quick).
938
+ // `!` prefix inserts at FRONT (urgent). Esc interrupts the running task.
939
+ const taskQueue = [];
940
+ let taskRunning = false;
941
+ let currentAbort = null;
935
942
  rt.onSubmit(() => {
936
943
  const line = rt.consumeInput().trim();
937
944
  if (!line)
938
945
  return;
939
- void handleLine(line);
946
+ if (line.startsWith('/')) {
947
+ void handleLine(line);
948
+ return;
949
+ }
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
+ if (taskRunning) {
965
+ taskQueue.push(line);
966
+ rt.addText(`📋 queued: "${line.slice(0, 60)}${line.length > 60 ? '…' : ''}" (${taskQueue.length} waiting)`, 'dim');
967
+ return;
968
+ }
969
+ void executeTaskQueue(line);
940
970
  });
971
+ async function executeTaskQueue(firstTask) {
972
+ taskRunning = true;
973
+ let task = firstTask;
974
+ while (task) {
975
+ await runSingleTask(task);
976
+ task = taskQueue.shift();
977
+ if (task)
978
+ rt.addText(`▶ next queued: "${task.slice(0, 60)}${task.length > 60 ? '…' : ''}"`, 'dim');
979
+ }
980
+ taskRunning = false;
981
+ }
982
+ async function runSingleTask(line) {
983
+ rt.addUser(line);
984
+ rt.setBusy(true, t.running);
985
+ currentAbort = new AbortController();
986
+ let appender = null;
987
+ let kind = null;
988
+ try {
989
+ const result = await runAgentTask({
990
+ task: line,
991
+ registry: reg,
992
+ cfg,
993
+ yes: autoApprove,
994
+ resumeMessages: history,
995
+ signal: currentAbort.signal,
996
+ approvalAsk: (name, args) => rt.requestApproval(name, args),
997
+ events: {
998
+ onLine: (l) => { if (kind === 'reasoning')
999
+ rt.foldThinking(); appender = null; kind = null; rt.addText(l, 'dim'); },
1000
+ onDelta: (k, chunk) => {
1001
+ if (k !== kind) {
1002
+ if (kind === 'reasoning')
1003
+ rt.foldThinking();
1004
+ appender = rt.startStream(k === 'reasoning' ? 'think' : 'say');
1005
+ kind = k;
1006
+ }
1007
+ appender?.(chunk);
1008
+ },
1009
+ onToolCall: (name, args) => {
1010
+ if (kind === 'reasoning')
1011
+ rt.foldThinking();
1012
+ appender = null;
1013
+ kind = null;
1014
+ const brief = name === 'run_command' && typeof args.command === 'string'
1015
+ ? args.command
1016
+ : JSON.stringify(args);
1017
+ rt.addText(`${YELLOW('●')} ${CYAN(name)} ${DIM(brief.replace(/\s+/g, ' ').slice(0, 90))}`);
1018
+ },
1019
+ onToolResult: (name, output, isError) => {
1020
+ const dot = isError ? RED('✗') : GREEN('•');
1021
+ const first = output.split('\n').find((l) => l.trim()) ?? '';
1022
+ rt.addText(` ${dot} ${DIM('⎿ ' + first.trim().slice(0, 100))}`);
1023
+ },
1024
+ },
1025
+ });
1026
+ rt.setBusy(false);
1027
+ rt.setStatus(`↑${result.usage.promptTokens} ↓${result.usage.completionTokens} tok · ${result.turns} turns · ${result.toolUses} tools`);
1028
+ history = [...history, { role: 'user', content: line }, ...result.messages.slice(history.length + 2)];
1029
+ }
1030
+ catch (err) {
1031
+ rt.setBusy(false);
1032
+ rt.addText(String(err), 'err');
1033
+ }
1034
+ finally {
1035
+ currentAbort = null;
1036
+ }
1037
+ }
941
1038
  async function handleLine(line) {
942
1039
  if (line === '/exit' || line === '/quit') {
943
1040
  rt.destroy();
@@ -945,6 +1042,31 @@ export async function tui(yes, noWeb = false) {
945
1042
  c.close();
946
1043
  process.exit(0);
947
1044
  }
1045
+ if (line === '/queue' || line.startsWith('/queue ')) {
1046
+ const sub = line.slice(7).trim();
1047
+ if (sub === 'clear') {
1048
+ const n = taskQueue.length;
1049
+ taskQueue.length = 0;
1050
+ rt.addText(n > 0 ? 'cleared ' + n + ' queued task(s)' : 'queue was already empty', 'dim');
1051
+ return;
1052
+ }
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
1063
+ const status = taskRunning ? 'running' : 'idle';
1064
+ const queueList = taskQueue.length > 0
1065
+ ? taskQueue.map((task, i) => ' ' + (i + 1) + '. ' + task.slice(0, 70)).join('\n')
1066
+ : ' (empty)';
1067
+ rt.addText('queue: ' + status + ' | ' + taskQueue.length + ' waiting\n' + queueList + '\n\ncommands: /queue clear, /queue skip, !<task> to insert at front', 'dim');
1068
+ return;
1069
+ }
948
1070
  if (line === '?' || line === '/help') {
949
1071
  rt.addText(COMMANDS.map((c) => ' ' + c.name.padEnd(11) + ' ' + String(t[c.key])).join('\n'), 'dim');
950
1072
  return;
@@ -1176,57 +1298,6 @@ export async function tui(yes, noWeb = false) {
1176
1298
  }
1177
1299
  return;
1178
1300
  }
1179
- rt.addUser(line);
1180
- rt.setBusy(true, t.running);
1181
- let appender = null;
1182
- let kind = null;
1183
- try {
1184
- const result = await runAgentTask({
1185
- task: line,
1186
- registry: reg,
1187
- cfg,
1188
- yes: autoApprove,
1189
- resumeMessages: history,
1190
- approvalAsk: (name, args) => rt.requestApproval(name, args),
1191
- events: {
1192
- onLine: (l) => { if (kind === 'reasoning')
1193
- rt.foldThinking(); appender = null; kind = null; rt.addText(l, 'dim'); },
1194
- onDelta: (k, chunk) => {
1195
- if (k !== kind) {
1196
- if (kind === 'reasoning')
1197
- rt.foldThinking(); // collapse before the next phase
1198
- appender = rt.startStream(k === 'reasoning' ? 'think' : 'say');
1199
- kind = k;
1200
- }
1201
- appender?.(chunk);
1202
- },
1203
- onToolCall: (name, args) => {
1204
- if (kind === 'reasoning')
1205
- rt.foldThinking();
1206
- appender = null;
1207
- kind = null;
1208
- // fold the args to their essence: for run_command the command
1209
- // string itself, otherwise a short JSON tail
1210
- const brief = name === 'run_command' && typeof args.command === 'string'
1211
- ? args.command
1212
- : JSON.stringify(args);
1213
- rt.addText(`${YELLOW('●')} ${CYAN(name)} ${DIM(brief.replace(/\s+/g, ' ').slice(0, 90))}`);
1214
- },
1215
- onToolResult: (name, output, isError) => {
1216
- const dot = isError ? RED('✗') : GREEN('•');
1217
- const first = output.split('\n').find((l) => l.trim()) ?? '';
1218
- rt.addText(` ${dot} ${DIM('⎿ ' + first.trim().slice(0, 100))}`);
1219
- },
1220
- },
1221
- });
1222
- rt.setBusy(false);
1223
- rt.setStatus(`↑${result.usage.promptTokens} ↓${result.usage.completionTokens} tok · ${result.turns} turns · ${result.toolUses} tools`);
1224
- history = [...history, { role: 'user', content: line }, ...result.messages.slice(history.length + 2)];
1225
- }
1226
- catch (err) {
1227
- rt.setBusy(false);
1228
- rt.addText(String(err), 'err');
1229
- }
1230
1301
  }
1231
1302
  await rt.waitExit();
1232
1303
  rt.destroy();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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.0",
47
- "@hmharness/evolution": "0.6.0",
48
- "@hmharness/domain-harmony": "0.6.0",
49
- "@hmharness/domain-ops": "0.6.0",
50
- "@hmharness/agent": "0.6.0",
51
- "@hmharness/web": "0.6.0"
46
+ "@hmharness/kernel": "0.6.2",
47
+ "@hmharness/evolution": "0.6.2",
48
+ "@hmharness/domain-harmony": "0.6.2",
49
+ "@hmharness/domain-ops": "0.6.2",
50
+ "@hmharness/agent": "0.6.2",
51
+ "@hmharness/web": "0.6.2"
52
52
  }
53
53
  }