@dotdrelle/wiki-manager 0.6.34 → 0.7.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/bin/wiki-manager.js +2 -2
- package/docker-compose.yml +25 -1
- package/package.json +2 -2
- package/src/agent/graph.js +2 -19
- package/src/cli/wiki-manager.js +236 -129
- package/src/commands/slash.js +17 -1
- package/src/commands/slash.test.js +78 -0
- package/src/core/activity.js +14 -0
- package/src/core/agentEvents.js +24 -2
- package/src/core/agentLoop.js +164 -0
- package/src/core/agentLoop.test.js +85 -0
- package/src/core/cacert.js +4 -3
- package/src/core/compose.js +4 -3
- package/src/core/dockerCompose.test.js +14 -0
- package/src/core/documentIntake.test.js +7 -7
- package/src/core/env.js +6 -0
- package/src/core/jobQueue.js +18 -4
- package/src/core/mcp.js +1 -1
- package/src/core/queueStore.js +27 -0
- package/src/core/queueStore.test.js +31 -0
- package/src/core/startupCheck.js +1 -1
- package/src/core/startupCheck.test.js +66 -0
- package/src/core/wikirc.js +2 -2
- package/src/core/workspaces.js +8 -1
- package/src/runtime/auth.js +35 -0
- package/src/runtime/auth.test.js +21 -0
- package/src/runtime/client.js +108 -0
- package/src/runtime/lifecycle.js +60 -0
- package/src/runtime/queueStore.js +6 -0
- package/src/runtime/runner.js +73 -0
- package/src/runtime/server.js +160 -0
- package/src/runtime/server.test.js +53 -0
- package/src/runtime/store.js +292 -0
- package/src/runtime/store.test.js +103 -0
- package/src/runtime/supervisor.js +98 -0
- package/src/runtime/supervisor.test.js +99 -0
- package/src/shell/repl.js +132 -17
- package/src/shell/repl.test.js +38 -0
- package/src/shell/tui.tsx +4 -1
- package/src/shell/useAgent.ts +20 -2
- package/src/shell/useSession.ts +146 -11
- package/wiki-workspace +39 -30
package/src/shell/repl.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { Transform } from 'node:stream';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
4
5
|
import { stdin as input, stdout as output } from 'node:process';
|
|
5
6
|
import { marked } from 'marked';
|
|
6
7
|
import { markedTerminal } from 'marked-terminal';
|
|
7
8
|
import { buildAgentSystemPrompt, buildLimitedAgentResponse } from '../agent/graph.js';
|
|
8
9
|
import { handleSlashCommand } from '../commands/slash.js';
|
|
9
10
|
import { serviceDescription, serviceNames as composeServiceNames } from '../core/compose.js';
|
|
10
|
-
import { extractActivity, sessionActivities } from '../core/activity.js';
|
|
11
|
+
import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
11
12
|
import { syncActivitiesToPlan } from '../core/plan.js';
|
|
12
13
|
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
13
14
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
14
15
|
import { listSkills } from '../core/skills.js';
|
|
15
16
|
import { listWikircProfiles } from '../core/wikirc.js';
|
|
16
17
|
import { listWorkspaces } from '../core/workspaces.js';
|
|
18
|
+
import { fetchRuntimeState, postRuntimeCancel, postRuntimeRun, streamRuntimeEvents } from '../runtime/client.js';
|
|
17
19
|
|
|
18
20
|
marked.use(markedTerminal());
|
|
19
21
|
// marked-terminal's text renderer extracts token.text (raw string) instead of
|
|
@@ -202,6 +204,7 @@ function completionValuesFor(parts, inputBuffer, session) {
|
|
|
202
204
|
if (tokenIndex === 0) return slashCompletions(session);
|
|
203
205
|
if (command === '/new' && tokenIndex === 1) return [];
|
|
204
206
|
if (command === '/use' && tokenIndex === 1) return workspaceNames();
|
|
207
|
+
if (command === '/use' && tokenIndex === 2) return [];
|
|
205
208
|
if (command === '/config' && tokenIndex === 1) return ['edit', 'list', 'status', 'use'];
|
|
206
209
|
if (command === '/config' && (previousToken === 'use' || previousToken === 'edit')) return wikircProfileNames(session);
|
|
207
210
|
if (command === '/mcp' && tokenIndex === 1) return ['call', 'endpoints', 'status', 'tools'];
|
|
@@ -668,14 +671,6 @@ function renderBannerWithMcpPanel(columns, session) {
|
|
|
668
671
|
|
|
669
672
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
670
673
|
|
|
671
|
-
function parseJsonText(text) {
|
|
672
|
-
try {
|
|
673
|
-
return JSON.parse(text);
|
|
674
|
-
} catch {
|
|
675
|
-
return null;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
674
|
function pathBaseName(value) {
|
|
680
675
|
return String(value ?? '').split('/').filter(Boolean).pop() ?? '';
|
|
681
676
|
}
|
|
@@ -737,6 +732,45 @@ function rememberProductionActivity(session, payload) {
|
|
|
737
732
|
return true;
|
|
738
733
|
}
|
|
739
734
|
|
|
735
|
+
export function applyRuntimeStateToShellSession(session, state) {
|
|
736
|
+
if (!state || typeof state !== 'object') return false;
|
|
737
|
+
session.agentProjection = {
|
|
738
|
+
conversation: Array.isArray(state.conversation) ? state.conversation.map((message) => ({ ...message })) : [],
|
|
739
|
+
chain: Array.isArray(state.chain) ? state.chain.map((step) => ({ ...step })) : [],
|
|
740
|
+
plan: Array.isArray(state.plan) ? state.plan.map((step) => ({ ...step })) : null,
|
|
741
|
+
activities: Array.isArray(state.activities) ? state.activities.map((activity) => ({ ...activity })) : [],
|
|
742
|
+
logs: Array.isArray(state.logs) ? [...state.logs] : [],
|
|
743
|
+
summary: state.summary ?? null,
|
|
744
|
+
status: state.status ?? 'idle',
|
|
745
|
+
};
|
|
746
|
+
session.headlessPlan = session.agentProjection.plan
|
|
747
|
+
? session.agentProjection.plan.map((step) => ({ ...step }))
|
|
748
|
+
: null;
|
|
749
|
+
session.activities = Object.fromEntries(
|
|
750
|
+
session.agentProjection.activities.map((activity) => [activity.key, { ...activity }]),
|
|
751
|
+
);
|
|
752
|
+
if (Array.isArray(state.queue)) session.jobQueue = state.queue.map((item) => ({ ...item }));
|
|
753
|
+
const production = session.agentProjection.activities.filter((activity) => activity.source === 'production').at(-1);
|
|
754
|
+
if (production) {
|
|
755
|
+
session.productionActivity = {
|
|
756
|
+
jobId: production.id,
|
|
757
|
+
status: production.status,
|
|
758
|
+
label: production.label,
|
|
759
|
+
terminal: production.terminal,
|
|
760
|
+
updatedAt: production.updatedAt,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
const runtimeConversation = session.agentProjection.conversation;
|
|
764
|
+
if (runtimeConversation.length > 0) {
|
|
765
|
+
session.conversations ??= { [GLOBAL_CONVERSATION_KEY]: [] };
|
|
766
|
+
session.conversations[conversationKey(session)] = runtimeConversation.map((message) => ({
|
|
767
|
+
role: message.role === 'assistant' ? 'donna' : message.role,
|
|
768
|
+
content: String(message.content ?? ''),
|
|
769
|
+
}));
|
|
770
|
+
}
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
|
|
740
774
|
function activityText(session) {
|
|
741
775
|
const activity = sessionActivities(session).find((item) => !item.terminal)
|
|
742
776
|
?? (session.productionActivity?.label ? session.productionActivity : null);
|
|
@@ -747,6 +781,10 @@ function activityText(session) {
|
|
|
747
781
|
return `${color}${truncateAnsi(activity.label, 72)}${styles.reset}`;
|
|
748
782
|
}
|
|
749
783
|
|
|
784
|
+
function runtimeRunActive(session) {
|
|
785
|
+
return String(session.agentProjection?.status ?? '').toLowerCase() === 'running';
|
|
786
|
+
}
|
|
787
|
+
|
|
750
788
|
function dividerWithActivity(session, columns) {
|
|
751
789
|
const activity = activityText(session);
|
|
752
790
|
if (!activity) return '─'.repeat(columns);
|
|
@@ -1141,7 +1179,7 @@ async function runPipeShell({ agent, packageJson, session }) {
|
|
|
1141
1179
|
let lastBodyLineCount = 0;
|
|
1142
1180
|
let lastMiddleHeight = 5;
|
|
1143
1181
|
|
|
1144
|
-
async function runTuiShell({ agent, packageJson, session }) {
|
|
1182
|
+
async function runTuiShell({ agent, packageJson, session, runtime = null }) {
|
|
1145
1183
|
const messages = conversationMessages(session);
|
|
1146
1184
|
messages.push({
|
|
1147
1185
|
role: 'donna',
|
|
@@ -1167,6 +1205,11 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1167
1205
|
let mouseSelectionTimer = null;
|
|
1168
1206
|
let done = false;
|
|
1169
1207
|
let processing = Promise.resolve();
|
|
1208
|
+
let runtimePollingActive = false;
|
|
1209
|
+
let runtimeStreamAbort = null;
|
|
1210
|
+
let runtimeReconnectTimer = null;
|
|
1211
|
+
let runtimeSyncTimer = null;
|
|
1212
|
+
let runtimeStreamStopped = false;
|
|
1170
1213
|
let finish;
|
|
1171
1214
|
const finished = new Promise((resolve) => {
|
|
1172
1215
|
finish = resolve;
|
|
@@ -1197,8 +1240,55 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1197
1240
|
rerender();
|
|
1198
1241
|
}
|
|
1199
1242
|
|
|
1243
|
+
function syncRuntimeState() {
|
|
1244
|
+
if (!runtime?.url) return;
|
|
1245
|
+
void fetchRuntimeState({ url: runtime.url })
|
|
1246
|
+
.then((state) => {
|
|
1247
|
+
runtimePollingActive = true;
|
|
1248
|
+
applyRuntimeStateToShellSession(session, state);
|
|
1249
|
+
rerender();
|
|
1250
|
+
})
|
|
1251
|
+
.catch(() => {
|
|
1252
|
+
runtimePollingActive = false;
|
|
1253
|
+
rerender();
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function scheduleRuntimeStateSync() {
|
|
1258
|
+
if (runtimeSyncTimer) clearTimeout(runtimeSyncTimer);
|
|
1259
|
+
runtimeSyncTimer = setTimeout(syncRuntimeState, 200);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
async function subscribeRuntimeEvents() {
|
|
1263
|
+
if (!runtime?.url || runtimeStreamStopped) return;
|
|
1264
|
+
runtimeStreamAbort = new AbortController();
|
|
1265
|
+
try {
|
|
1266
|
+
for await (const event of streamRuntimeEvents({ url: runtime.url, signal: runtimeStreamAbort.signal })) {
|
|
1267
|
+
runtimePollingActive = true;
|
|
1268
|
+
if (event.type === 'state') {
|
|
1269
|
+
applyRuntimeStateToShellSession(session, event.data);
|
|
1270
|
+
rerender();
|
|
1271
|
+
} else {
|
|
1272
|
+
scheduleRuntimeStateSync();
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
} catch {
|
|
1276
|
+
// Runtime stream may be temporarily unavailable; the local MCP polling fallback resumes below.
|
|
1277
|
+
}
|
|
1278
|
+
if (runtimeStreamStopped) return;
|
|
1279
|
+
runtimePollingActive = false;
|
|
1280
|
+
rerender();
|
|
1281
|
+
runtimeReconnectTimer = setTimeout(() => { void subscribeRuntimeEvents(); }, 1500);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
if (runtime?.url) {
|
|
1285
|
+
syncRuntimeState();
|
|
1286
|
+
void subscribeRuntimeEvents();
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1200
1289
|
const pollBusy = new Set();
|
|
1201
1290
|
const productionPollInterval = setInterval(async () => {
|
|
1291
|
+
if (runtimePollingActive) return;
|
|
1202
1292
|
const candidates = sessionActivities(session).filter((item) => item.poll && !item.terminal);
|
|
1203
1293
|
// Legacy fallback: if no generic activities tracked yet, fall back to productionActivity.
|
|
1204
1294
|
if (candidates.length === 0 && session.productionActivity?.jobId && !session.productionActivity.terminal) {
|
|
@@ -1306,14 +1396,12 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1306
1396
|
}
|
|
1307
1397
|
|
|
1308
1398
|
if (key?.ctrl && key.name === 'y') {
|
|
1309
|
-
const messages = conversationMessages(session);
|
|
1310
1399
|
const lastDonna = [...messages].reverse().find((m) => isDonnaRole(m.role));
|
|
1311
1400
|
if (lastDonna) {
|
|
1312
1401
|
const text = stripAnsi(colorizeStatus(lastDonna.content)).replace(/\[[0-9;]*m/g, '');
|
|
1313
1402
|
const clipCmd = process.platform === 'darwin'
|
|
1314
1403
|
? { command: 'pbcopy', args: [] }
|
|
1315
1404
|
: { command: 'xclip', args: ['-selection', 'clipboard'] };
|
|
1316
|
-
const { execFileSync } = await import('node:child_process');
|
|
1317
1405
|
const prevLabel = spinnerLabel;
|
|
1318
1406
|
const prevBusy = busy;
|
|
1319
1407
|
try {
|
|
@@ -1335,6 +1423,18 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1335
1423
|
rerender();
|
|
1336
1424
|
return;
|
|
1337
1425
|
}
|
|
1426
|
+
if (runtime?.url && runtimeRunActive(session)) {
|
|
1427
|
+
void postRuntimeCancel({ url: runtime.url })
|
|
1428
|
+
.then(() => {
|
|
1429
|
+
activityLines = [...activityLines, 'runtime: cancel requested'].slice(-LOWER_DETAIL_ROWS);
|
|
1430
|
+
rerender();
|
|
1431
|
+
})
|
|
1432
|
+
.catch((err) => {
|
|
1433
|
+
activityLines = [...activityLines, `runtime cancel error: ${err instanceof Error ? err.message : String(err)}`].slice(-LOWER_DETAIL_ROWS);
|
|
1434
|
+
rerender();
|
|
1435
|
+
});
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1338
1438
|
const now = Date.now();
|
|
1339
1439
|
if (now - lastCtrlCAt <= 1500) {
|
|
1340
1440
|
done = true;
|
|
@@ -1382,9 +1482,20 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1382
1482
|
session._abortSignal = currentAbortController.signal;
|
|
1383
1483
|
let aborted = false;
|
|
1384
1484
|
try {
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1485
|
+
if (runtime?.url && !session.chatMode && !line.trim().startsWith('/')) {
|
|
1486
|
+
await postRuntimeRun(line, {
|
|
1487
|
+
url: runtime.url,
|
|
1488
|
+
workspace: session.workspace ?? null,
|
|
1489
|
+
});
|
|
1490
|
+
conversationMessages(session).push({ role: 'user', content: line });
|
|
1491
|
+
conversationMessages(session).push({ role: 'command', content: `Runtime run queued: ${runtime.url}` });
|
|
1492
|
+
activityLines = [...activityLines, 'runtime: run accepted'].slice(-LOWER_DETAIL_ROWS);
|
|
1493
|
+
syncRuntimeState();
|
|
1494
|
+
} else {
|
|
1495
|
+
const result = await runLine(line, { agent, packageJson, session, onUpdate: rerender, onStep });
|
|
1496
|
+
done = result.exit;
|
|
1497
|
+
aborted = result.aborted ?? false;
|
|
1498
|
+
}
|
|
1388
1499
|
} catch (err) {
|
|
1389
1500
|
if (err.name !== 'AbortError') throw err;
|
|
1390
1501
|
aborted = true;
|
|
@@ -1479,6 +1590,10 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1479
1590
|
await processing;
|
|
1480
1591
|
} finally {
|
|
1481
1592
|
mouseFilter.off('keypress', onKeypress);
|
|
1593
|
+
runtimeStreamStopped = true;
|
|
1594
|
+
runtimeStreamAbort?.abort();
|
|
1595
|
+
clearTimeout(runtimeReconnectTimer);
|
|
1596
|
+
clearTimeout(runtimeSyncTimer);
|
|
1482
1597
|
clearInterval(productionPollInterval);
|
|
1483
1598
|
clearTimeout(ctrlCTimer);
|
|
1484
1599
|
clearTimeout(mouseSelectionTimer);
|
|
@@ -1492,11 +1607,11 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1492
1607
|
}
|
|
1493
1608
|
}
|
|
1494
1609
|
|
|
1495
|
-
export async function runShell({ agent, packageJson }) {
|
|
1610
|
+
export async function runShell({ agent, packageJson, runtime = null }) {
|
|
1496
1611
|
const session = createSession();
|
|
1497
1612
|
if (!input.isTTY || !output.isTTY) {
|
|
1498
1613
|
await runPipeShell({ agent, packageJson, session });
|
|
1499
1614
|
return;
|
|
1500
1615
|
}
|
|
1501
|
-
|
|
1616
|
+
await runTuiShell({ agent, packageJson, session, runtime });
|
|
1502
1617
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { applyRuntimeStateToShellSession, createSession, conversationMessages } from './repl.js';
|
|
4
|
+
|
|
5
|
+
test('applyRuntimeStateToShellSession projects runtime state into shell session', () => {
|
|
6
|
+
const session = createSession();
|
|
7
|
+
session.workspace = 'docs';
|
|
8
|
+
|
|
9
|
+
const applied = applyRuntimeStateToShellSession(session, {
|
|
10
|
+
status: 'running',
|
|
11
|
+
conversation: [
|
|
12
|
+
{ role: 'user', content: 'Build docs' },
|
|
13
|
+
{ role: 'assistant', content: 'Working.' },
|
|
14
|
+
],
|
|
15
|
+
plan: [{ step: 1, description: 'Build', status: 'running' }],
|
|
16
|
+
activities: [{
|
|
17
|
+
key: 'production:job-1',
|
|
18
|
+
id: 'job-1',
|
|
19
|
+
source: 'production',
|
|
20
|
+
label: 'Production: build',
|
|
21
|
+
status: 'running',
|
|
22
|
+
terminal: false,
|
|
23
|
+
}],
|
|
24
|
+
queue: [{ id: 'q-1', status: 'waiting' }],
|
|
25
|
+
logs: ['agentic-loop: turn 1/20'],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
assert.equal(applied, true);
|
|
29
|
+
assert.equal(session.agentProjection.status, 'running');
|
|
30
|
+
assert.equal(session.headlessPlan[0].description, 'Build');
|
|
31
|
+
assert.equal(session.activities['production:job-1'].status, 'running');
|
|
32
|
+
assert.equal(session.productionActivity.jobId, 'job-1');
|
|
33
|
+
assert.equal(session.jobQueue[0].id, 'q-1');
|
|
34
|
+
assert.deepEqual(conversationMessages(session), [
|
|
35
|
+
{ role: 'user', content: 'Build docs' },
|
|
36
|
+
{ role: 'donna', content: 'Working.' },
|
|
37
|
+
]);
|
|
38
|
+
});
|
package/src/shell/tui.tsx
CHANGED
|
@@ -41,6 +41,7 @@ function copyToClipboard(text: string, renderer: unknown) {
|
|
|
41
41
|
function App(props: {
|
|
42
42
|
agent: unknown;
|
|
43
43
|
packageJson: Record<string, unknown>;
|
|
44
|
+
runtime?: any;
|
|
44
45
|
}) {
|
|
45
46
|
const renderer = useRenderer();
|
|
46
47
|
const dimensions = useTerminalDimensions();
|
|
@@ -204,11 +205,13 @@ function App(props: {
|
|
|
204
205
|
export async function runOpenTuiShell({
|
|
205
206
|
agent,
|
|
206
207
|
packageJson,
|
|
208
|
+
runtime = null,
|
|
207
209
|
}: {
|
|
208
210
|
agent: unknown;
|
|
209
211
|
packageJson: Record<string, unknown>;
|
|
212
|
+
runtime?: any;
|
|
210
213
|
}) {
|
|
211
|
-
await render(() => <App agent={agent} packageJson={packageJson} />, {
|
|
214
|
+
await render(() => <App agent={agent} packageJson={packageJson} runtime={runtime} />, {
|
|
212
215
|
exitOnCtrlC: false,
|
|
213
216
|
useMouse: true,
|
|
214
217
|
targetFps: 30,
|
package/src/shell/useAgent.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createSignal } from 'solid-js';
|
|
2
|
-
import {
|
|
2
|
+
import { postRuntimeCancel, postRuntimeRun } from '../runtime/client.js';
|
|
3
|
+
import { conversationMessages, runLine } from './repl.js';
|
|
3
4
|
|
|
4
|
-
export function useAgent(props: { agent: unknown; packageJson: Record<string, unknown>; session: Record<string, any>; chatMode: () => boolean; refresh: () => void; addLog: (line: string) => void }) {
|
|
5
|
+
export function useAgent(props: { agent: unknown; packageJson: Record<string, unknown>; session: Record<string, any>; chatMode: () => boolean; runtimeUrl?: string | null; refresh: () => void; addLog: (line: string) => void; onRuntimeAccepted?: () => void }) {
|
|
5
6
|
const [busy, setBusy] = createSignal(false);
|
|
6
7
|
const [abortController, setAbortController] = createSignal<AbortController | null>(null);
|
|
7
8
|
|
|
@@ -17,6 +18,18 @@ export function useAgent(props: { agent: unknown; packageJson: Record<string, un
|
|
|
17
18
|
props.addLog(`input: ${trimmed}`);
|
|
18
19
|
|
|
19
20
|
try {
|
|
21
|
+
if (props.runtimeUrl && !props.chatMode() && !trimmed.startsWith('/')) {
|
|
22
|
+
await postRuntimeRun(trimmed, {
|
|
23
|
+
url: props.runtimeUrl,
|
|
24
|
+
workspace: props.session.workspace ?? null,
|
|
25
|
+
});
|
|
26
|
+
conversationMessages(props.session).push({ role: 'user', content: trimmed });
|
|
27
|
+
conversationMessages(props.session).push({ role: 'command', content: `Runtime run queued: ${props.runtimeUrl}` });
|
|
28
|
+
props.onRuntimeAccepted?.();
|
|
29
|
+
props.addLog('runtime: run accepted');
|
|
30
|
+
props.refresh();
|
|
31
|
+
return { exit: false, runtime: true };
|
|
32
|
+
}
|
|
20
33
|
const result = await runLine(trimmed, {
|
|
21
34
|
agent: props.agent,
|
|
22
35
|
packageJson: props.packageJson,
|
|
@@ -39,6 +52,11 @@ export function useAgent(props: { agent: unknown; packageJson: Record<string, un
|
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
function abort() {
|
|
55
|
+
if (props.runtimeUrl) {
|
|
56
|
+
void postRuntimeCancel({ url: props.runtimeUrl })
|
|
57
|
+
.then(() => props.addLog('runtime: cancel requested'))
|
|
58
|
+
.catch((err) => props.addLog(`runtime cancel error: ${err instanceof Error ? err.message : String(err)}`));
|
|
59
|
+
}
|
|
42
60
|
abortController()?.abort();
|
|
43
61
|
props.addLog('interrupt requested');
|
|
44
62
|
}
|
package/src/shell/useSession.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { extractActivity, parseJsonText, sessionActivities } from '../core/activ
|
|
|
6
6
|
import { formatPlanStatus, formatCompletedActivities } from '../core/plan.js';
|
|
7
7
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
8
8
|
import { queueCounts, startNextQueuedJob, syncQueueWithActivity } from '../core/jobQueue.js';
|
|
9
|
+
import { queueStoreFor } from '../core/queueStore.js';
|
|
10
|
+
import { fetchRuntimeState, streamRuntimeEvents } from '../runtime/client.js';
|
|
9
11
|
import type { ActiveFileEditor } from './FileEditorDialog';
|
|
10
12
|
import {
|
|
11
13
|
completionContext,
|
|
@@ -15,6 +17,16 @@ import {
|
|
|
15
17
|
} from './repl.js';
|
|
16
18
|
import { useAgent } from './useAgent';
|
|
17
19
|
|
|
20
|
+
function runtimeStatusText(status: 'disabled' | 'connected' | 'disconnected', runStatus: string): string {
|
|
21
|
+
if (status === 'connected') return `runtime ${runStatus}`;
|
|
22
|
+
if (status === 'disconnected') return 'runtime offline';
|
|
23
|
+
return 'runtime off';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function nonEmptyRuntimeArray<T>(value: T[] | undefined | null): T[] | null {
|
|
27
|
+
return Array.isArray(value) && value.length > 0 ? value : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
function buildContinuationPrompt(session: any, completed: any[]): string {
|
|
19
31
|
const originalTask = [...conversationMessages(session)]
|
|
20
32
|
.reverse()
|
|
@@ -31,10 +43,12 @@ function buildContinuationPrompt(session: any, completed: any[]): string {
|
|
|
31
43
|
].filter(Boolean).join('\n');
|
|
32
44
|
}
|
|
33
45
|
|
|
34
|
-
export function useSession(props: { agent: unknown; packageJson: Record<string, unknown
|
|
46
|
+
export function useSession(props: { agent: unknown; packageJson: Record<string, unknown>; runtime?: any }) {
|
|
35
47
|
const session = createSession();
|
|
36
48
|
const [version, setVersion] = createSignal(0);
|
|
37
49
|
const [logs, setLogs] = createSignal<string[]>([]);
|
|
50
|
+
const [runtimeState, setRuntimeState] = createSignal<any | null>(null);
|
|
51
|
+
const [runtimeStatus, setRuntimeStatus] = createSignal<'disabled' | 'connected' | 'disconnected'>(props.runtime ? 'disconnected' : 'disabled');
|
|
38
52
|
const [input, setInput] = createSignal('');
|
|
39
53
|
const [chatMode, setChatMode] = createSignal(true);
|
|
40
54
|
(session as any).chatMode = true;
|
|
@@ -60,10 +74,45 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
60
74
|
const addLog = (line: string) => {
|
|
61
75
|
setLogs((items) => [...items, `${new Date().toLocaleTimeString()} ${line}`].slice(-200));
|
|
62
76
|
};
|
|
63
|
-
|
|
77
|
+
if (props.runtime?.url) addLog(`runtime: connected ${props.runtime.url}${props.runtime.started ? ' (started)' : ''}`);
|
|
78
|
+
const agent = useAgent({
|
|
79
|
+
agent: props.agent,
|
|
80
|
+
packageJson: props.packageJson,
|
|
81
|
+
session,
|
|
82
|
+
chatMode,
|
|
83
|
+
runtimeUrl: props.runtime?.url ?? null,
|
|
84
|
+
refresh,
|
|
85
|
+
addLog,
|
|
86
|
+
onRuntimeAccepted: () => {
|
|
87
|
+
setRuntimeState((state) => ({ ...(state ?? {}), status: 'running' }));
|
|
88
|
+
setRuntimeStatus('connected');
|
|
89
|
+
refresh();
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const runtimeRunStatus = createMemo(() => String(runtimeState()?.status ?? 'idle'));
|
|
93
|
+
const agentBusy = createMemo(() =>
|
|
94
|
+
props.runtime?.url
|
|
95
|
+
? runtimeRunStatus() === 'running' || agent.busy()
|
|
96
|
+
: agent.busy(),
|
|
97
|
+
);
|
|
98
|
+
const localFallbackActive = createMemo(() => !props.runtime?.url || runtimeStatus() === 'disconnected');
|
|
64
99
|
|
|
65
100
|
const messages = createMemo(() => {
|
|
66
101
|
version();
|
|
102
|
+
const runtimeConversation = runtimeState()?.conversation;
|
|
103
|
+
const localCommands = conversationMessages(session)
|
|
104
|
+
.filter((message: any) => message.role === 'command')
|
|
105
|
+
.map((message: any) => ({
|
|
106
|
+
role: 'command',
|
|
107
|
+
content: String(message.content ?? ''),
|
|
108
|
+
}));
|
|
109
|
+
if (Array.isArray(runtimeConversation) && runtimeConversation.length > 0) {
|
|
110
|
+
const runtimeMessages = runtimeConversation.map((message: any) => ({
|
|
111
|
+
role: message.role === 'assistant' ? 'donna' : message.role,
|
|
112
|
+
content: String(message.content ?? ''),
|
|
113
|
+
}));
|
|
114
|
+
return [...runtimeMessages, ...localCommands].slice(-200);
|
|
115
|
+
}
|
|
67
116
|
return [...conversationMessages(session)];
|
|
68
117
|
});
|
|
69
118
|
const prompt = createMemo(() => {
|
|
@@ -84,6 +133,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
84
133
|
session.wikirc?.profile ? session.wikirc.profile : 'no wikirc',
|
|
85
134
|
session.language ? session.language : 'no language',
|
|
86
135
|
session.llm ? 'llm ready' : 'llm limited',
|
|
136
|
+
runtimeStatusText(runtimeStatus(), runtimeRunStatus()),
|
|
87
137
|
].join(' ');
|
|
88
138
|
});
|
|
89
139
|
const matchContext = createMemo(() => {
|
|
@@ -119,37 +169,117 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
119
169
|
let lastVisiblePlan: Array<{ step: number; description: string; status: string }> | null = null;
|
|
120
170
|
const activities = createMemo(() => {
|
|
121
171
|
version();
|
|
172
|
+
const runtimeActivities = nonEmptyRuntimeArray(runtimeState()?.activities);
|
|
173
|
+
if (runtimeActivities) {
|
|
174
|
+
return runtimeActivities.map((activity: any) => ({ ...activity, _runtime: true }));
|
|
175
|
+
}
|
|
122
176
|
const current = sessionActivities(session);
|
|
123
177
|
if (current.length > 0) {
|
|
124
178
|
lastVisibleActivities = current.map((activity) => ({ ...activity }));
|
|
125
179
|
return current;
|
|
126
180
|
}
|
|
127
|
-
return
|
|
181
|
+
return agentBusy() && lastVisibleActivities.length > 0
|
|
128
182
|
? lastVisibleActivities.map((activity) => ({ ...activity }))
|
|
129
183
|
: current;
|
|
130
184
|
});
|
|
131
185
|
const queueItems = createMemo(() => {
|
|
132
186
|
version();
|
|
187
|
+
const runtimeQueue = nonEmptyRuntimeArray(runtimeState()?.queue);
|
|
188
|
+
if (runtimeQueue) return runtimeQueue.map((item: any) => ({ ...item, _runtime: true }));
|
|
133
189
|
return ((session as any).jobQueue ?? []).map((item: any) => ({ ...item }));
|
|
134
190
|
});
|
|
135
191
|
const queueInfo = createMemo(() => {
|
|
136
192
|
version();
|
|
193
|
+
const runtimeQueue = runtimeState()?.queue;
|
|
194
|
+
if (Array.isArray(runtimeQueue)) {
|
|
195
|
+
return {
|
|
196
|
+
active: runtimeQueue.filter((item: any) => ['waiting', 'starting', 'running', 'queued', 'pending'].includes(String(item.status ?? '').toLowerCase())).length,
|
|
197
|
+
current: runtimeQueue.filter((item: any) => ['starting', 'running'].includes(String(item.status ?? '').toLowerCase())).length,
|
|
198
|
+
frozen: 0,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
137
201
|
return queueCounts(session);
|
|
138
202
|
});
|
|
139
203
|
const plan = createMemo(() => {
|
|
140
204
|
version();
|
|
205
|
+
const runtimePlan = nonEmptyRuntimeArray(runtimeState()?.plan);
|
|
206
|
+
if (runtimePlan) {
|
|
207
|
+
return runtimePlan.map((step: any, index: number) => ({
|
|
208
|
+
step: Number(step.step ?? index + 1),
|
|
209
|
+
description: String(step.description ?? step.label ?? step.name ?? `Step ${index + 1}`),
|
|
210
|
+
status: String(step.status ?? 'pending'),
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
141
213
|
const p = (session as any).headlessPlan as Array<{ step: number; description: string; status: string }> | null;
|
|
142
214
|
const current = p ? p.map((s) => ({ ...s })) : null;
|
|
143
215
|
if (current && current.length > 0) {
|
|
144
216
|
lastVisiblePlan = current.map((step) => ({ ...step }));
|
|
145
217
|
return current;
|
|
146
218
|
}
|
|
147
|
-
return
|
|
219
|
+
return agentBusy() && lastVisiblePlan && lastVisiblePlan.length > 0
|
|
148
220
|
? lastVisiblePlan.map((step) => ({ ...step }))
|
|
149
221
|
: current;
|
|
150
222
|
});
|
|
223
|
+
const visibleLogs = createMemo(() => {
|
|
224
|
+
version();
|
|
225
|
+
const runtimeLogs = runtimeState()?.logs;
|
|
226
|
+
if (!Array.isArray(runtimeLogs) || runtimeLogs.length === 0) return logs();
|
|
227
|
+
const tagged = runtimeLogs.slice(-80).map((line: any) => `runtime ${String(line)}`);
|
|
228
|
+
return [...logs(), ...tagged].slice(-200);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
function syncRuntimeState() {
|
|
232
|
+
void fetchRuntimeState({ url: props.runtime.url })
|
|
233
|
+
.then((state) => {
|
|
234
|
+
setRuntimeState(state);
|
|
235
|
+
setRuntimeStatus('connected');
|
|
236
|
+
refresh();
|
|
237
|
+
})
|
|
238
|
+
.catch(() => {
|
|
239
|
+
setRuntimeStatus('disconnected');
|
|
240
|
+
refresh();
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
let runtimeStreamAbort: AbortController | null = null;
|
|
244
|
+
let runtimeSyncDebounce: ReturnType<typeof setTimeout> | null = null;
|
|
245
|
+
let runtimeReconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
246
|
+
let runtimeStreamStopped = false;
|
|
247
|
+
|
|
248
|
+
function debouncedSyncRuntimeState() {
|
|
249
|
+
if (runtimeSyncDebounce) clearTimeout(runtimeSyncDebounce);
|
|
250
|
+
runtimeSyncDebounce = setTimeout(syncRuntimeState, 200);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function subscribeRuntimeEvents() {
|
|
254
|
+
if (!props.runtime?.url || runtimeStreamStopped) return;
|
|
255
|
+
runtimeStreamAbort = new AbortController();
|
|
256
|
+
try {
|
|
257
|
+
for await (const _event of streamRuntimeEvents({ url: props.runtime.url, signal: runtimeStreamAbort.signal })) {
|
|
258
|
+
setRuntimeStatus('connected');
|
|
259
|
+
debouncedSyncRuntimeState();
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
// stream dropped or errored — fall through to reconnect below
|
|
263
|
+
}
|
|
264
|
+
if (runtimeStreamStopped) return;
|
|
265
|
+
setRuntimeStatus('disconnected');
|
|
266
|
+
refresh();
|
|
267
|
+
runtimeReconnectTimer = setTimeout(() => { void subscribeRuntimeEvents(); }, 1500);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (props.runtime?.url) {
|
|
271
|
+
syncRuntimeState();
|
|
272
|
+
void subscribeRuntimeEvents();
|
|
273
|
+
}
|
|
274
|
+
onCleanup(() => {
|
|
275
|
+
runtimeStreamStopped = true;
|
|
276
|
+
runtimeStreamAbort?.abort();
|
|
277
|
+
if (runtimeSyncDebounce) clearTimeout(runtimeSyncDebounce);
|
|
278
|
+
if (runtimeReconnectTimer) clearTimeout(runtimeReconnectTimer);
|
|
279
|
+
});
|
|
151
280
|
|
|
152
281
|
const activityPollTimer = setInterval(() => {
|
|
282
|
+
if (!localFallbackActive()) return;
|
|
153
283
|
for (const activity of sessionActivities(session)) {
|
|
154
284
|
if (activity.terminal || !activity.poll) continue;
|
|
155
285
|
const key = activity.key ?? `${activity.poll.server}:${activity.id ?? activity.label}`;
|
|
@@ -195,7 +325,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
195
325
|
const plan = (session as any).headlessPlan;
|
|
196
326
|
const stillRunning = sessionActivities(session).filter((a) => !a.terminal && a.poll);
|
|
197
327
|
const pendingSteps = (plan ?? []).filter((s: any) => s.status === 'pending');
|
|
198
|
-
if (stillRunning.length === 0 && pendingSteps.length > 0 && !
|
|
328
|
+
if (stillRunning.length === 0 && pendingSteps.length > 0 && !agentBusy()) {
|
|
199
329
|
const completedAll = sessionActivities(session).filter((a) => a.terminal);
|
|
200
330
|
const prompt = buildContinuationPrompt(session, completedAll);
|
|
201
331
|
void agent.submit(prompt);
|
|
@@ -211,18 +341,23 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
211
341
|
});
|
|
212
342
|
}
|
|
213
343
|
}, 1000);
|
|
214
|
-
onCleanup(() =>
|
|
344
|
+
onCleanup(() => {
|
|
345
|
+
if (activityPollTimer) clearInterval(activityPollTimer);
|
|
346
|
+
});
|
|
215
347
|
|
|
216
348
|
const queueFallbackTimer = setInterval(() => {
|
|
217
|
-
if ((
|
|
349
|
+
if (!localFallbackActive()) return;
|
|
350
|
+
if (queueStoreFor(session).list().some((item: any) => item.status === 'waiting')) {
|
|
218
351
|
void startNextQueuedJob(session, { addLog, refresh });
|
|
219
352
|
}
|
|
220
353
|
}, 10000);
|
|
221
|
-
onCleanup(() =>
|
|
354
|
+
onCleanup(() => {
|
|
355
|
+
if (queueFallbackTimer) clearInterval(queueFallbackTimer);
|
|
356
|
+
});
|
|
222
357
|
|
|
223
358
|
async function submitInput(submittedValue?: string) {
|
|
224
359
|
const line = typeof submittedValue === 'string' && submittedValue.trim() ? submittedValue : input();
|
|
225
|
-
if (
|
|
360
|
+
if (agentBusy()) return { exit: false, busy: true };
|
|
226
361
|
setInput('');
|
|
227
362
|
setConversationScroll(0);
|
|
228
363
|
if (line.trim()) {
|
|
@@ -344,7 +479,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
344
479
|
return {
|
|
345
480
|
session,
|
|
346
481
|
messages,
|
|
347
|
-
logs,
|
|
482
|
+
logs: visibleLogs,
|
|
348
483
|
input,
|
|
349
484
|
setInput: updateInput,
|
|
350
485
|
title,
|
|
@@ -364,7 +499,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
364
499
|
plan,
|
|
365
500
|
conversationScroll,
|
|
366
501
|
scrollConversation,
|
|
367
|
-
busy:
|
|
502
|
+
busy: agentBusy,
|
|
368
503
|
abort,
|
|
369
504
|
submitInput,
|
|
370
505
|
completeSelected,
|