@dotdrelle/wiki-manager 0.12.1 → 0.12.7
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/package.json +2 -2
- package/src/activity/activityAggregator.js +34 -3
- package/src/activity/activityAggregator.test.js +32 -0
- package/src/agent/graph.js +272 -22
- package/src/agent/graph.test.js +357 -1
- package/src/cli/wiki-manager.js +24 -1
- package/src/commands/slash.js +74 -1
- package/src/commands/slash.test.js +36 -0
- package/src/core/activity.js +4 -0
- package/src/core/activity.test.js +9 -1
- package/src/core/agentEvents.js +30 -0
- package/src/core/agentEvents.test.js +42 -1
- package/src/core/buildInfo.js +58 -0
- package/src/core/buildInfo.json +4 -0
- package/src/core/buildInfo.test.js +14 -0
- package/src/core/mcp.js +21 -1
- package/src/core/mcp.test.js +25 -0
- package/src/core/runtimeLog.js +6 -1
- package/src/core/runtimeLog.test.js +3 -1
- package/src/runtime/client.js +22 -0
- package/src/runtime/controlMessages.js +2 -2
- package/src/runtime/recoveryManager.js +54 -0
- package/src/runtime/recoveryManager.test.js +73 -0
- package/src/runtime/runner.js +49 -13
- package/src/runtime/runner.test.js +196 -0
- package/src/runtime/server.js +67 -8
- package/src/runtime/server.test.js +223 -6
- package/src/runtime/store.js +48 -3
- package/src/runtime/store.test.js +32 -0
- package/src/runtime/supervisor.js +77 -1
- package/src/shell/RightPane.tsx +124 -31
- package/src/shell/SetupWizard.tsx +13 -1
- package/src/shell/repl.js +78 -9
- package/src/shell/repl.test.js +114 -1
- package/src/shell/tui.tsx +4 -1
- package/src/shell/useAgent.ts +32 -3
- package/src/shell/useSession.ts +14 -1
package/src/shell/repl.test.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
runLine,
|
|
12
12
|
runtimeStatusLine,
|
|
13
13
|
runtimeUnavailableAgentMessage,
|
|
14
|
+
shouldHandleFreeTextLocally,
|
|
14
15
|
submitRuntimeRun,
|
|
15
16
|
} from './repl.js';
|
|
16
17
|
|
|
@@ -81,7 +82,9 @@ test('submitRuntimeRun reports acceptance without throwing', async () => {
|
|
|
81
82
|
try {
|
|
82
83
|
const session = createSession();
|
|
83
84
|
const outcome = await submitRuntimeRun('build the doc', { runtime: { url: 'http://runtime.test' }, session });
|
|
84
|
-
|
|
85
|
+
// The accepted payload is passed through so callers can surface the runId
|
|
86
|
+
// in the chat (immediate feedback that the run started).
|
|
87
|
+
assert.deepEqual(outcome, { kind: 'accepted', result: { accepted: true, runId: 'run-1' } });
|
|
85
88
|
} finally {
|
|
86
89
|
restore();
|
|
87
90
|
}
|
|
@@ -191,3 +194,113 @@ test('runtime status exposes the disconnected reason', () => {
|
|
|
191
194
|
'⚠ Runtime indisponible : token mismatch — /agent désactivé, /chat reste possible',
|
|
192
195
|
);
|
|
193
196
|
});
|
|
197
|
+
|
|
198
|
+
test('/run kill posts to the runtime kill endpoint', async () => {
|
|
199
|
+
let calledUrl = null;
|
|
200
|
+
const restore = stubFetch(async (url) => {
|
|
201
|
+
calledUrl = new URL(String(url));
|
|
202
|
+
return jsonResponse(202, { killed: true, runs: 1, tasks: 2 });
|
|
203
|
+
});
|
|
204
|
+
try {
|
|
205
|
+
const session = createSession();
|
|
206
|
+
session.workspace = 'docs';
|
|
207
|
+
|
|
208
|
+
const result = await runLine('/run kill', {
|
|
209
|
+
agent: null,
|
|
210
|
+
packageJson: { version: 'test' },
|
|
211
|
+
session,
|
|
212
|
+
runtime: { url: 'http://runtime.test' },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
assert.equal(result.exit, false);
|
|
216
|
+
assert.equal(calledUrl.pathname, '/kill');
|
|
217
|
+
assert.equal(calledUrl.searchParams.get('workspace'), 'docs');
|
|
218
|
+
assert.match(conversationMessages(session).at(-1).content, /Runtime kill requested: 1 run, 2 tasks cancelled/);
|
|
219
|
+
} finally {
|
|
220
|
+
restore();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('/queue cancel on a runtime workflow id points to run cancellation commands', async () => {
|
|
225
|
+
const session = createSession();
|
|
226
|
+
session.workspace = 'docs';
|
|
227
|
+
session.jobQueue = [];
|
|
228
|
+
session.workflow = {
|
|
229
|
+
nodes: [{ id: 'task:runtime-a', type: 'task', label: 'Runtime task', status: 'pending' }],
|
|
230
|
+
relations: [],
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const result = await runLine('/queue cancel task:runtime-a', {
|
|
234
|
+
agent: null,
|
|
235
|
+
packageJson: { version: 'test' },
|
|
236
|
+
session,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
assert.equal(result.exit, false);
|
|
240
|
+
assert.match(conversationMessages(session).at(-1).content, /Item géré par le runtime/);
|
|
241
|
+
assert.match(conversationMessages(session).at(-1).content, /\/run kill/);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('free text routing keeps questions local and sends actions to the runtime', () => {
|
|
245
|
+
const session = createSession();
|
|
246
|
+
session.llm = { completeWithTools: () => {} };
|
|
247
|
+
|
|
248
|
+
// The original incident: a config question must never start a run.
|
|
249
|
+
const question = shouldHandleFreeTextLocally('donne moi la config du cme', session);
|
|
250
|
+
assert.equal(question.local, true);
|
|
251
|
+
assert.equal(question.classification.kind, 'observe');
|
|
252
|
+
|
|
253
|
+
const smallTalk = shouldHandleFreeTextLocally('bonjour', session);
|
|
254
|
+
assert.equal(smallTalk.local, true);
|
|
255
|
+
|
|
256
|
+
const action = shouldHandleFreeTextLocally('lance le pipeline complet', session);
|
|
257
|
+
assert.equal(action.local, false);
|
|
258
|
+
assert.equal(action.classification.kind, 'start_run');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('free text routing keeps questions local even during an active run', () => {
|
|
262
|
+
// The chat must stay available during a run: a status question or small
|
|
263
|
+
// talk answered locally (read-only tools) — never enqueued as a future run.
|
|
264
|
+
const session = createSession();
|
|
265
|
+
session.llm = { completeWithTools: () => {} };
|
|
266
|
+
session.agentProjection = { status: 'running', activities: [], conversation: [] };
|
|
267
|
+
assert.equal(shouldHandleFreeTextLocally('où en est le run', session).local, true);
|
|
268
|
+
assert.equal(shouldHandleFreeTextLocally('salut', session).local, true);
|
|
269
|
+
// Cancel intents are handled by Donna locally (runtime__kill/cancel tools);
|
|
270
|
+
// approvals stay on the deterministic control lane.
|
|
271
|
+
assert.equal(shouldHandleFreeTextLocally('stop le job', session).local, true);
|
|
272
|
+
assert.equal(shouldHandleFreeTextLocally('supprime le job et la queue', session).local, true);
|
|
273
|
+
// Approvals and "later" requests too: Donna owns runtime__approve and
|
|
274
|
+
// runtime__enqueue. Only plan modifications and new runs bypass her.
|
|
275
|
+
assert.equal(shouldHandleFreeTextLocally('approuve le run', session).local, true);
|
|
276
|
+
assert.equal(shouldHandleFreeTextLocally('fais le build plus tard', session).local, true);
|
|
277
|
+
|
|
278
|
+
const offline = createSession();
|
|
279
|
+
offline.llm = null;
|
|
280
|
+
const fallback = shouldHandleFreeTextLocally('donne moi la config du cme', offline);
|
|
281
|
+
assert.equal(fallback.local, false);
|
|
282
|
+
assert.match(fallback.fallbackReason ?? '', /LLM unavailable/);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('submitRuntimeRun sends a control message instead of /run while a run is active', async () => {
|
|
286
|
+
const paths = [];
|
|
287
|
+
const restore = stubFetch(async (url) => {
|
|
288
|
+
paths.push(pathOf(url));
|
|
289
|
+
return jsonResponse(200, {
|
|
290
|
+
accepted: true,
|
|
291
|
+
kind: 'cancel',
|
|
292
|
+
classification: { kind: 'cancel' },
|
|
293
|
+
explanation: 'Runtime cancellation requested.',
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
try {
|
|
297
|
+
const session = createSession();
|
|
298
|
+
session.agentProjection = { status: 'running', activities: [], conversation: [] };
|
|
299
|
+
const outcome = await submitRuntimeRun('stop le job', { runtime: { url: 'http://runtime.test' }, session });
|
|
300
|
+
assert.deepEqual(paths, ['/control'], 'active run must use the control lane, not POST /run');
|
|
301
|
+
assert.equal(outcome.kind, 'cancel');
|
|
302
|
+
assert.match(outcome.result?.explanation ?? '', /cancellation requested/i);
|
|
303
|
+
} finally {
|
|
304
|
+
restore();
|
|
305
|
+
}
|
|
306
|
+
});
|
package/src/shell/tui.tsx
CHANGED
|
@@ -126,7 +126,10 @@ function App(props: {
|
|
|
126
126
|
const conversationRows = createMemo(() => Math.max(4, dimensions().height - 5 - chatInputHeight()));
|
|
127
127
|
const rightColumns = createMemo(() => {
|
|
128
128
|
const width = dimensions().width;
|
|
129
|
-
|
|
129
|
+
// 38% / cap 56 (was 32% / cap 44): the Plan/Activity/Logs panes carry job
|
|
130
|
+
// labels, file names and error messages — 40 columns truncated everything
|
|
131
|
+
// into unreadable stubs.
|
|
132
|
+
return Math.max(30, Math.min(56, Math.floor(width * 0.38)));
|
|
130
133
|
});
|
|
131
134
|
const leftColumns = createMemo(() => Math.max(32, dimensions().width - rightColumns() - 1));
|
|
132
135
|
const conversationColumns = createMemo(() => {
|
package/src/shell/useAgent.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSignal } from 'solid-js';
|
|
2
2
|
import { postRuntimeCancel } from '../runtime/client.js';
|
|
3
|
-
import { conversationMessages, recordRuntimeUnavailableAgentInput, runLine, submitRuntimeRun } from './repl.js';
|
|
3
|
+
import { conversationMessages, recordRuntimeUnavailableAgentInput, runLine, shouldHandleFreeTextLocally, submitRuntimeRun } from './repl.js';
|
|
4
4
|
|
|
5
5
|
export function useAgent(props: { agent: unknown; packageJson: Record<string, unknown>; session: Record<string, any>; chatMode: () => boolean; runtimeUrl?: string | null; runtimeUnavailableReason?: string | null; refresh: () => void; addLog: (line: string) => void; onRuntimeAccepted?: () => void }) {
|
|
6
6
|
const [busy, setBusy] = createSignal(false);
|
|
@@ -18,7 +18,17 @@ export function useAgent(props: { agent: unknown; packageJson: Record<string, un
|
|
|
18
18
|
props.addLog(`input: ${trimmed}`);
|
|
19
19
|
|
|
20
20
|
try {
|
|
21
|
-
|
|
21
|
+
// Questions and small talk are answered by the local agent: the reply
|
|
22
|
+
// shows up in the chat immediately and no runtime run (plan, SQLite
|
|
23
|
+
// state, replans) is created for a sentence that only needed an answer.
|
|
24
|
+
// Actions still go to the runtime below.
|
|
25
|
+
const freeTextRouting = (props.runtimeUrl && !props.chatMode() && !trimmed.startsWith('/'))
|
|
26
|
+
? shouldHandleFreeTextLocally(trimmed, props.session)
|
|
27
|
+
: null;
|
|
28
|
+
if (props.runtimeUrl && !props.chatMode() && !trimmed.startsWith('/') && !freeTextRouting?.local) {
|
|
29
|
+
if (freeTextRouting?.fallbackReason) {
|
|
30
|
+
props.addLog(`runtime: ${freeTextRouting.fallbackReason}, routing to runtime run`);
|
|
31
|
+
}
|
|
22
32
|
// Marked _pending so mergeRuntimeConversation (useSession.ts) can
|
|
23
33
|
// confirm this exact entry instead of pushing a second copy once the
|
|
24
34
|
// same user message comes back from the runtime's own /state.
|
|
@@ -29,6 +39,13 @@ export function useAgent(props: { agent: unknown; packageJson: Record<string, un
|
|
|
29
39
|
});
|
|
30
40
|
if (outcome.kind === 'accepted') {
|
|
31
41
|
props.onRuntimeAccepted?.();
|
|
42
|
+
const runId = (outcome as any).result?.runId ?? null;
|
|
43
|
+
// Light, immediate feedback in the chat: without it an accepted run
|
|
44
|
+
// is invisible until its first activity lands in the side panels.
|
|
45
|
+
conversationMessages(props.session).push({
|
|
46
|
+
role: 'command',
|
|
47
|
+
content: `▶ Run accepté${runId ? ` (${String(runId).slice(0, 8)})` : ''} — progression dans le panneau Activity, réponse ici à la fin du run.`,
|
|
48
|
+
});
|
|
32
49
|
props.addLog('runtime: run accepted');
|
|
33
50
|
} else if (outcome.kind === 'queued') {
|
|
34
51
|
// The server localizes control-lane acknowledgements from the
|
|
@@ -37,6 +54,11 @@ export function useAgent(props: { agent: unknown; packageJson: Record<string, un
|
|
|
37
54
|
const explanation = (outcome as any).result?.explanation ?? 'Request added to the queue.';
|
|
38
55
|
conversationMessages(props.session).push({ role: 'command', content: String(explanation) });
|
|
39
56
|
props.addLog('runtime: control queued');
|
|
57
|
+
} else if ((outcome as any).result?.explanation) {
|
|
58
|
+
// Control-lane kinds (cancel / approve / observe / modify_run…):
|
|
59
|
+
// surface the server's localized explanation instead of an error.
|
|
60
|
+
conversationMessages(props.session).push({ role: 'command', content: String((outcome as any).result.explanation) });
|
|
61
|
+
props.addLog(`runtime: ${outcome.kind}`);
|
|
40
62
|
} else {
|
|
41
63
|
conversationMessages(props.session).push({ role: 'command', content: `Runtime error: ${outcome.message}` });
|
|
42
64
|
props.addLog(`runtime error: ${outcome.message}`);
|
|
@@ -44,7 +66,7 @@ export function useAgent(props: { agent: unknown; packageJson: Record<string, un
|
|
|
44
66
|
props.refresh();
|
|
45
67
|
return { exit: false, runtime: true };
|
|
46
68
|
}
|
|
47
|
-
if (!props.chatMode() && !trimmed.startsWith('/')) {
|
|
69
|
+
if (!props.chatMode() && !trimmed.startsWith('/') && !freeTextRouting?.local) {
|
|
48
70
|
const message = recordRuntimeUnavailableAgentInput(props.session, trimmed, {
|
|
49
71
|
error: props.runtimeUnavailableReason ?? 'runtime introuvable',
|
|
50
72
|
});
|
|
@@ -52,6 +74,9 @@ export function useAgent(props: { agent: unknown; packageJson: Record<string, un
|
|
|
52
74
|
props.refresh();
|
|
53
75
|
return { exit: false, runtimeUnavailable: true };
|
|
54
76
|
}
|
|
77
|
+
if (freeTextRouting?.local) {
|
|
78
|
+
props.addLog(`agent: ${freeTextRouting.classification.kind} handled locally`);
|
|
79
|
+
}
|
|
55
80
|
const result = await runLine(trimmed, {
|
|
56
81
|
agent: props.agent,
|
|
57
82
|
packageJson: props.packageJson,
|
|
@@ -59,6 +84,10 @@ export function useAgent(props: { agent: unknown; packageJson: Record<string, un
|
|
|
59
84
|
onUpdate: props.refresh,
|
|
60
85
|
onStep: props.addLog,
|
|
61
86
|
chatMode: props.chatMode(),
|
|
87
|
+
// Without this, slash commands that target the runtime (/run kill,
|
|
88
|
+
// /run cancel, /run status) reported "Runtime unavailable" in the
|
|
89
|
+
// TUI even while the status bar showed "runtime: connected".
|
|
90
|
+
runtime: props.runtimeUrl ? { url: props.runtimeUrl } : null,
|
|
62
91
|
});
|
|
63
92
|
props.refresh();
|
|
64
93
|
return result;
|
package/src/shell/useSession.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
|
|
4
4
|
import { formatMcpToolResult, callMcpTool } from '../core/mcp.js';
|
|
5
|
+
import { versionWithBuild } from '../core/buildInfo.js';
|
|
5
6
|
import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
6
7
|
import { formatPlanStatus, formatCompletedActivities, formatPlanStep } from '../core/plan.js';
|
|
7
8
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
@@ -47,6 +48,12 @@ function buildContinuationPrompt(session: any, completed: any[]): string {
|
|
|
47
48
|
|
|
48
49
|
export function useSession(props: { agent: unknown; packageJson: Record<string, unknown>; runtime?: any }) {
|
|
49
50
|
const session = createSession();
|
|
51
|
+
// Donna's runtime__kill/cancel/status/approve/enqueue tools (graph.js)
|
|
52
|
+
// read session.runtime.url directly — without this the OpenTUI session
|
|
53
|
+
// never had it set (only the legacy runShell path did), so those tools
|
|
54
|
+
// reported "Runtime not connected" even with the runtime up and the
|
|
55
|
+
// status bar showing "connected".
|
|
56
|
+
(session as any).runtime = props.runtime ?? null;
|
|
50
57
|
const [version, setVersion] = createSignal(0);
|
|
51
58
|
const [logs, setLogs] = createSignal<string[]>([]);
|
|
52
59
|
const [runtimeState, setRuntimeState] = createSignal<any | null>(null);
|
|
@@ -139,7 +146,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
139
146
|
const statusLine = createMemo(() => {
|
|
140
147
|
version();
|
|
141
148
|
return [
|
|
142
|
-
`wiki-manager ${props.packageJson
|
|
149
|
+
`wiki-manager ${versionWithBuild(props.packageJson)}`.trim(),
|
|
143
150
|
session.workspace ? session.workspace : 'no workspace',
|
|
144
151
|
session.wikirc?.profile ? session.wikirc.profile : 'no wikirc',
|
|
145
152
|
session.language ? session.language : 'no language',
|
|
@@ -337,6 +344,12 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
337
344
|
const target = conversationMessages(session);
|
|
338
345
|
const backed = runtimeConversationRefsByWorkspace.get(workspace) ?? [];
|
|
339
346
|
runtimeConversationRefsByWorkspace.set(workspace, backed);
|
|
347
|
+
// The merge is index-aligned with the runtime conversation array. If that
|
|
348
|
+
// array got SHORTER (runtime restart, projection reset), keeping stale
|
|
349
|
+
// refs would make every new runtime entry silently overwrite an old
|
|
350
|
+
// backed entry instead of appearing in the chat. Drop the refs and let
|
|
351
|
+
// the new conversation append cleanly.
|
|
352
|
+
if (runtimeConversation.length < backed.length) backed.length = 0;
|
|
340
353
|
if (backed.length === 0 && target.length === 0 && !runtimeConversation.some((message: any) => message?._pending)) {
|
|
341
354
|
backed.push(...runtimeConversation.map((message: any) => ({
|
|
342
355
|
role: message.role === 'assistant' ? 'donna' : message.role,
|