@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.
- package/dist/tui.js +123 -52
- 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
|
-
|
|
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.
|
|
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.
|
|
47
|
-
"@hmharness/evolution": "0.6.
|
|
48
|
-
"@hmharness/domain-harmony": "0.6.
|
|
49
|
-
"@hmharness/domain-ops": "0.6.
|
|
50
|
-
"@hmharness/agent": "0.6.
|
|
51
|
-
"@hmharness/web": "0.6.
|
|
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
|
}
|