@dotdrelle/wiki-manager 0.6.47 → 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/cli/wiki-manager.js +236 -129
- 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 +2 -1
- 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/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 +131 -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 +23 -18
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
|
|
@@ -669,14 +671,6 @@ function renderBannerWithMcpPanel(columns, session) {
|
|
|
669
671
|
|
|
670
672
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
671
673
|
|
|
672
|
-
function parseJsonText(text) {
|
|
673
|
-
try {
|
|
674
|
-
return JSON.parse(text);
|
|
675
|
-
} catch {
|
|
676
|
-
return null;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
674
|
function pathBaseName(value) {
|
|
681
675
|
return String(value ?? '').split('/').filter(Boolean).pop() ?? '';
|
|
682
676
|
}
|
|
@@ -738,6 +732,45 @@ function rememberProductionActivity(session, payload) {
|
|
|
738
732
|
return true;
|
|
739
733
|
}
|
|
740
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
|
+
|
|
741
774
|
function activityText(session) {
|
|
742
775
|
const activity = sessionActivities(session).find((item) => !item.terminal)
|
|
743
776
|
?? (session.productionActivity?.label ? session.productionActivity : null);
|
|
@@ -748,6 +781,10 @@ function activityText(session) {
|
|
|
748
781
|
return `${color}${truncateAnsi(activity.label, 72)}${styles.reset}`;
|
|
749
782
|
}
|
|
750
783
|
|
|
784
|
+
function runtimeRunActive(session) {
|
|
785
|
+
return String(session.agentProjection?.status ?? '').toLowerCase() === 'running';
|
|
786
|
+
}
|
|
787
|
+
|
|
751
788
|
function dividerWithActivity(session, columns) {
|
|
752
789
|
const activity = activityText(session);
|
|
753
790
|
if (!activity) return '─'.repeat(columns);
|
|
@@ -1142,7 +1179,7 @@ async function runPipeShell({ agent, packageJson, session }) {
|
|
|
1142
1179
|
let lastBodyLineCount = 0;
|
|
1143
1180
|
let lastMiddleHeight = 5;
|
|
1144
1181
|
|
|
1145
|
-
async function runTuiShell({ agent, packageJson, session }) {
|
|
1182
|
+
async function runTuiShell({ agent, packageJson, session, runtime = null }) {
|
|
1146
1183
|
const messages = conversationMessages(session);
|
|
1147
1184
|
messages.push({
|
|
1148
1185
|
role: 'donna',
|
|
@@ -1168,6 +1205,11 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1168
1205
|
let mouseSelectionTimer = null;
|
|
1169
1206
|
let done = false;
|
|
1170
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;
|
|
1171
1213
|
let finish;
|
|
1172
1214
|
const finished = new Promise((resolve) => {
|
|
1173
1215
|
finish = resolve;
|
|
@@ -1198,8 +1240,55 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1198
1240
|
rerender();
|
|
1199
1241
|
}
|
|
1200
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
|
+
|
|
1201
1289
|
const pollBusy = new Set();
|
|
1202
1290
|
const productionPollInterval = setInterval(async () => {
|
|
1291
|
+
if (runtimePollingActive) return;
|
|
1203
1292
|
const candidates = sessionActivities(session).filter((item) => item.poll && !item.terminal);
|
|
1204
1293
|
// Legacy fallback: if no generic activities tracked yet, fall back to productionActivity.
|
|
1205
1294
|
if (candidates.length === 0 && session.productionActivity?.jobId && !session.productionActivity.terminal) {
|
|
@@ -1307,14 +1396,12 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1307
1396
|
}
|
|
1308
1397
|
|
|
1309
1398
|
if (key?.ctrl && key.name === 'y') {
|
|
1310
|
-
const messages = conversationMessages(session);
|
|
1311
1399
|
const lastDonna = [...messages].reverse().find((m) => isDonnaRole(m.role));
|
|
1312
1400
|
if (lastDonna) {
|
|
1313
1401
|
const text = stripAnsi(colorizeStatus(lastDonna.content)).replace(/\[[0-9;]*m/g, '');
|
|
1314
1402
|
const clipCmd = process.platform === 'darwin'
|
|
1315
1403
|
? { command: 'pbcopy', args: [] }
|
|
1316
1404
|
: { command: 'xclip', args: ['-selection', 'clipboard'] };
|
|
1317
|
-
const { execFileSync } = await import('node:child_process');
|
|
1318
1405
|
const prevLabel = spinnerLabel;
|
|
1319
1406
|
const prevBusy = busy;
|
|
1320
1407
|
try {
|
|
@@ -1336,6 +1423,18 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1336
1423
|
rerender();
|
|
1337
1424
|
return;
|
|
1338
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
|
+
}
|
|
1339
1438
|
const now = Date.now();
|
|
1340
1439
|
if (now - lastCtrlCAt <= 1500) {
|
|
1341
1440
|
done = true;
|
|
@@ -1383,9 +1482,20 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1383
1482
|
session._abortSignal = currentAbortController.signal;
|
|
1384
1483
|
let aborted = false;
|
|
1385
1484
|
try {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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
|
+
}
|
|
1389
1499
|
} catch (err) {
|
|
1390
1500
|
if (err.name !== 'AbortError') throw err;
|
|
1391
1501
|
aborted = true;
|
|
@@ -1480,6 +1590,10 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1480
1590
|
await processing;
|
|
1481
1591
|
} finally {
|
|
1482
1592
|
mouseFilter.off('keypress', onKeypress);
|
|
1593
|
+
runtimeStreamStopped = true;
|
|
1594
|
+
runtimeStreamAbort?.abort();
|
|
1595
|
+
clearTimeout(runtimeReconnectTimer);
|
|
1596
|
+
clearTimeout(runtimeSyncTimer);
|
|
1483
1597
|
clearInterval(productionPollInterval);
|
|
1484
1598
|
clearTimeout(ctrlCTimer);
|
|
1485
1599
|
clearTimeout(mouseSelectionTimer);
|
|
@@ -1493,11 +1607,11 @@ async function runTuiShell({ agent, packageJson, session }) {
|
|
|
1493
1607
|
}
|
|
1494
1608
|
}
|
|
1495
1609
|
|
|
1496
|
-
export async function runShell({ agent, packageJson }) {
|
|
1610
|
+
export async function runShell({ agent, packageJson, runtime = null }) {
|
|
1497
1611
|
const session = createSession();
|
|
1498
1612
|
if (!input.isTTY || !output.isTTY) {
|
|
1499
1613
|
await runPipeShell({ agent, packageJson, session });
|
|
1500
1614
|
return;
|
|
1501
1615
|
}
|
|
1502
|
-
|
|
1616
|
+
await runTuiShell({ agent, packageJson, session, runtime });
|
|
1503
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,
|