@dotdrelle/wiki-manager 0.8.2 → 0.10.4
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/README.md +36 -6
- package/agents.docker-compose.yml +5 -0
- package/package.json +5 -2
- package/src/agent/graph.js +68 -9
- package/src/agent/graph.test.js +64 -0
- package/src/cli/wiki-manager.js +34 -2
- package/src/commands/slash.js +9 -25
- package/src/contracts/README.md +16 -0
- package/src/contracts/schemas.js +302 -0
- package/src/contracts/schemas.test.js +93 -0
- package/src/core/activity.js +14 -2
- package/src/core/activity.test.js +4 -2
- package/src/core/agentEvents.js +212 -24
- package/src/core/agentEvents.test.js +85 -12
- package/src/core/agentLoop.js +32 -7
- package/src/core/mcp.js +3 -1
- package/src/core/plan.js +4 -0
- package/src/core/planPatch.js +224 -0
- package/src/core/planPatch.test.js +63 -0
- package/src/core/sessionConfig.js +24 -0
- package/src/core/workflow.js +264 -0
- package/src/core/workflow.test.js +66 -0
- package/src/runtime/client.js +28 -1
- package/src/runtime/runner.js +432 -20
- package/src/runtime/runner.test.js +273 -1
- package/src/runtime/server.js +494 -24
- package/src/runtime/server.test.js +661 -0
- package/src/runtime/store.js +59 -7
- package/src/runtime/store.test.js +72 -1
- package/src/shell/RightPane.tsx +1 -7
- package/src/shell/StartupScreen.tsx +212 -0
- package/src/shell/repl.js +51 -7
- package/src/shell/repl.test.js +77 -1
- package/src/shell/textFit.ts +6 -0
- package/src/shell/tui.tsx +163 -0
- package/src/shell/useAgent.ts +17 -9
- package/src/shell/useSession.ts +34 -0
package/src/shell/tui.tsx
CHANGED
|
@@ -7,10 +7,70 @@ import { LeftPane } from './LeftPane';
|
|
|
7
7
|
import { RightPane } from './RightPane';
|
|
8
8
|
import { SlashDialog } from './SlashDialog';
|
|
9
9
|
import { SetupWizard } from './SetupWizard';
|
|
10
|
+
import { StartupScreen, type StartupAction } from './StartupScreen';
|
|
10
11
|
import { useSession } from './useSession';
|
|
12
|
+
import { buildMcpStatus } from '../core/mcp.js';
|
|
13
|
+
import { loadWikircProfile, summarizeWikircConfig } from '../core/wikirc.js';
|
|
14
|
+
import { listWorkspaces } from '../core/workspaces.js';
|
|
11
15
|
|
|
12
16
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
13
17
|
|
|
18
|
+
function emptyStartupInfo(version: string, workspace: { name: string } | null, workspaces: { name: string }[]) {
|
|
19
|
+
return {
|
|
20
|
+
version,
|
|
21
|
+
model: '',
|
|
22
|
+
connectedMcpServers: 0,
|
|
23
|
+
wikiReady: false,
|
|
24
|
+
workspaceName: workspace?.name ?? null,
|
|
25
|
+
profileName: 'default',
|
|
26
|
+
workspaces: workspaces.map((item) => item.name),
|
|
27
|
+
hasWorkspace: workspace != null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function startupInfo(packageJson: Record<string, unknown>) {
|
|
32
|
+
// listWorkspaces() order is filesystem-dependent (readdirSync), not stable —
|
|
33
|
+
// sort so "the default workspace" is deterministic across runs/platforms.
|
|
34
|
+
const workspaces = [...listWorkspaces()].sort((a, b) => a.name.localeCompare(b.name));
|
|
35
|
+
const workspace = workspaces[0] ?? null;
|
|
36
|
+
const version = String(packageJson.version ?? '');
|
|
37
|
+
if (!workspace) return emptyStartupInfo(version, null, []);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const loaded = loadWikircProfile(workspace.workspacePath, 'default');
|
|
41
|
+
const summary = summarizeWikircConfig(loaded.profile, loaded.config);
|
|
42
|
+
const session = {
|
|
43
|
+
workspace: workspace.name,
|
|
44
|
+
workspacePath: workspace.workspacePath,
|
|
45
|
+
workspaceEnv: workspace.env,
|
|
46
|
+
wikirc: {
|
|
47
|
+
profile: loaded.profile.name,
|
|
48
|
+
fileName: loaded.profile.fileName,
|
|
49
|
+
path: loaded.profile.path,
|
|
50
|
+
},
|
|
51
|
+
wikircConfig: loaded.config,
|
|
52
|
+
};
|
|
53
|
+
const mcp = buildMcpStatus(session);
|
|
54
|
+
const connectedMcpServers = Object.values(mcp)
|
|
55
|
+
.filter((server: any) => server?.status && server.status !== 'missing')
|
|
56
|
+
.length;
|
|
57
|
+
const provider = summary.provider ? String(summary.provider) : '';
|
|
58
|
+
const model = summary.model ? String(summary.model) : '';
|
|
59
|
+
return {
|
|
60
|
+
version,
|
|
61
|
+
model: [provider, model].filter(Boolean).join(' / '),
|
|
62
|
+
connectedMcpServers,
|
|
63
|
+
wikiReady: true,
|
|
64
|
+
workspaceName: workspace.name,
|
|
65
|
+
profileName: loaded.profile.name,
|
|
66
|
+
workspaces: workspaces.map((item) => item.name),
|
|
67
|
+
hasWorkspace: true,
|
|
68
|
+
};
|
|
69
|
+
} catch {
|
|
70
|
+
return emptyStartupInfo(version, workspace, workspaces);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
14
74
|
function copyToClipboard(text: string, renderer: unknown) {
|
|
15
75
|
try {
|
|
16
76
|
if ((renderer as any).copyToClipboardOSC52?.(text)) return true;
|
|
@@ -49,11 +109,16 @@ function App(props: {
|
|
|
49
109
|
const [exitHint, setExitHint] = createSignal(false);
|
|
50
110
|
const [copyHint, setCopyHint] = createSignal<string | null>(null);
|
|
51
111
|
const [chatInputHeight, setChatInputHeight] = createSignal(3);
|
|
112
|
+
// The app has exactly three mutually exclusive screens; one signal makes
|
|
113
|
+
// that invariant structural instead of relying on two booleans staying in
|
|
114
|
+
// sync at every call site.
|
|
115
|
+
const [screen, setScreen] = createSignal<'startup' | 'setup' | 'main'>('startup');
|
|
52
116
|
let ctrlCTimer: ReturnType<typeof setTimeout> | null = null;
|
|
53
117
|
let copyHintTimer: ReturnType<typeof setTimeout> | null = null;
|
|
54
118
|
let selectionCopyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
55
119
|
let lastCopiedSelection = '';
|
|
56
120
|
const state = useSession(props);
|
|
121
|
+
const startup = createMemo(() => startupInfo(props.packageJson));
|
|
57
122
|
const conversationRows = createMemo(() => Math.max(4, dimensions().height - 5 - chatInputHeight()));
|
|
58
123
|
const rightColumns = createMemo(() => {
|
|
59
124
|
const width = dimensions().width;
|
|
@@ -73,6 +138,71 @@ function App(props: {
|
|
|
73
138
|
});
|
|
74
139
|
};
|
|
75
140
|
|
|
141
|
+
const loadWorkspace = async (workspaceName?: string | null) => {
|
|
142
|
+
if (!workspaceName) return false;
|
|
143
|
+
if ((state.session as any).workspace === workspaceName) return true;
|
|
144
|
+
await state.submitInput(`/use ${workspaceName}`);
|
|
145
|
+
// /use does not throw on failure (e.g. a stale/deleted workspace) — it
|
|
146
|
+
// just returns an error message without switching session.workspace, so
|
|
147
|
+
// callers must check the actual post-await state rather than assume success.
|
|
148
|
+
return (state.session as any).workspace === workspaceName;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// `startup` is a memo over non-reactive fs reads (listWorkspaces() etc.),
|
|
152
|
+
// so it only ever reflects state at first mount. Action handlers that
|
|
153
|
+
// decide *which* workspace to load must re-read current state directly
|
|
154
|
+
// (startupInfo(...)) rather than trust the frozen memo, the same way
|
|
155
|
+
// closeSetup() already does.
|
|
156
|
+
const loadDefaultWorkspace = async () => loadWorkspace(startupInfo(props.packageJson).workspaceName);
|
|
157
|
+
|
|
158
|
+
// "Open a workspace" is really one composite action (load it, then bring
|
|
159
|
+
// its services up) — named here so it has one definition instead of being
|
|
160
|
+
// inlined as three sequential submitInput calls in openAction.
|
|
161
|
+
const openWorkspaceAndStartServices = async (workspaceName?: string | null) => {
|
|
162
|
+
const loaded = await loadWorkspace(workspaceName);
|
|
163
|
+
if (loaded) {
|
|
164
|
+
await state.submitInput('/start agents');
|
|
165
|
+
await state.submitInput('/start all');
|
|
166
|
+
}
|
|
167
|
+
return loaded;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const openAction = (action: StartupAction, workspaceName?: string) => {
|
|
171
|
+
if (action === 'init-workspace') {
|
|
172
|
+
setScreen('setup');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
setScreen('main');
|
|
176
|
+
void (async () => {
|
|
177
|
+
try {
|
|
178
|
+
if (action === 'open-workspace') {
|
|
179
|
+
await openWorkspaceAndStartServices(workspaceName ?? startupInfo(props.packageJson).workspaceName);
|
|
180
|
+
} else if (action === 'new-conversation' || action === 'run-workflow') {
|
|
181
|
+
const loaded = await loadDefaultWorkspace();
|
|
182
|
+
if (action === 'run-workflow' && loaded) {
|
|
183
|
+
await state.submitInput('/agent');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// Individual submitInput failures already surface their own error
|
|
188
|
+
// text in the conversation transcript; just stop the sequence here
|
|
189
|
+
// instead of leaving an unhandled rejection.
|
|
190
|
+
}
|
|
191
|
+
})();
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const closeSetup = () => {
|
|
195
|
+
const info = startupInfo(props.packageJson);
|
|
196
|
+
if (!info.hasWorkspace) {
|
|
197
|
+
setScreen('startup');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
setScreen('main');
|
|
201
|
+
// Reuse loadWorkspace (not a bare /use dispatch) so the "already on this
|
|
202
|
+
// workspace" short-circuit applies here too.
|
|
203
|
+
void loadWorkspace(info.workspaceName);
|
|
204
|
+
};
|
|
205
|
+
|
|
76
206
|
const showCopyHint = (message: string) => {
|
|
77
207
|
setCopyHint(message);
|
|
78
208
|
if (copyHintTimer) clearTimeout(copyHintTimer);
|
|
@@ -106,6 +236,7 @@ function App(props: {
|
|
|
106
236
|
|
|
107
237
|
useKeyboard((key) => {
|
|
108
238
|
const keyName = String(key.name ?? '').toLowerCase();
|
|
239
|
+
if (screen() !== 'main') return;
|
|
109
240
|
if (state.activeEditor()) {
|
|
110
241
|
if (keyName === 'escape') state.closeEditor();
|
|
111
242
|
return;
|
|
@@ -151,6 +282,26 @@ function App(props: {
|
|
|
151
282
|
return null;
|
|
152
283
|
};
|
|
153
284
|
|
|
285
|
+
if (screen() === 'startup') {
|
|
286
|
+
const info = startup();
|
|
287
|
+
return (
|
|
288
|
+
<StartupScreen
|
|
289
|
+
version={info.version}
|
|
290
|
+
model={info.model}
|
|
291
|
+
connectedMcpServers={info.connectedMcpServers}
|
|
292
|
+
wikiReady={info.wikiReady}
|
|
293
|
+
workspaceName={info.workspaceName}
|
|
294
|
+
profileName={info.profileName}
|
|
295
|
+
workspaces={info.workspaces}
|
|
296
|
+
hasWorkspace={info.hasWorkspace}
|
|
297
|
+
width={dimensions().width}
|
|
298
|
+
height={dimensions().height}
|
|
299
|
+
onSelect={openAction}
|
|
300
|
+
onQuit={() => renderer.destroy()}
|
|
301
|
+
/>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
154
305
|
return (
|
|
155
306
|
<box width="100%" height="100%" flexDirection="row">
|
|
156
307
|
<LeftPane
|
|
@@ -198,6 +349,18 @@ function App(props: {
|
|
|
198
349
|
onSave={state.saveEditor}
|
|
199
350
|
onCancel={state.closeEditor}
|
|
200
351
|
/>
|
|
352
|
+
{screen() === 'setup' ? (
|
|
353
|
+
<SetupWizard
|
|
354
|
+
mode="setup"
|
|
355
|
+
session={state.session}
|
|
356
|
+
width={dimensions().width}
|
|
357
|
+
height={dimensions().height}
|
|
358
|
+
initialRoute="workspace-name"
|
|
359
|
+
closeOnDone
|
|
360
|
+
onComplete={closeSetup}
|
|
361
|
+
onClose={closeSetup}
|
|
362
|
+
/>
|
|
363
|
+
) : null}
|
|
201
364
|
</box>
|
|
202
365
|
);
|
|
203
366
|
}
|
package/src/shell/useAgent.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSignal } from 'solid-js';
|
|
2
|
-
import { postRuntimeCancel
|
|
3
|
-
import { conversationMessages, runLine } from './repl.js';
|
|
2
|
+
import { postRuntimeCancel } from '../runtime/client.js';
|
|
3
|
+
import { conversationMessages, runLine, 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; refresh: () => void; addLog: (line: string) => void; onRuntimeAccepted?: () => void }) {
|
|
6
6
|
const [busy, setBusy] = createSignal(false);
|
|
@@ -19,14 +19,22 @@ export function useAgent(props: { agent: unknown; packageJson: Record<string, un
|
|
|
19
19
|
|
|
20
20
|
try {
|
|
21
21
|
if (props.runtimeUrl && !props.chatMode() && !trimmed.startsWith('/')) {
|
|
22
|
-
await postRuntimeRun(trimmed, {
|
|
23
|
-
url: props.runtimeUrl,
|
|
24
|
-
workspace: props.session.workspace ?? null,
|
|
25
|
-
});
|
|
26
22
|
conversationMessages(props.session).push({ role: 'user', content: trimmed });
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
const outcome = await submitRuntimeRun(trimmed, {
|
|
24
|
+
runtime: { url: props.runtimeUrl },
|
|
25
|
+
session: props.session,
|
|
26
|
+
});
|
|
27
|
+
if (outcome.kind === 'accepted') {
|
|
28
|
+
conversationMessages(props.session).push({ role: 'command', content: `Runtime run queued: ${props.runtimeUrl}` });
|
|
29
|
+
props.onRuntimeAccepted?.();
|
|
30
|
+
props.addLog('runtime: run accepted');
|
|
31
|
+
} else if (outcome.kind === 'queued') {
|
|
32
|
+
conversationMessages(props.session).push({ role: 'command', content: 'Runtime is busy — request added to the control queue, it will start automatically.' });
|
|
33
|
+
props.addLog('runtime: control queued');
|
|
34
|
+
} else {
|
|
35
|
+
conversationMessages(props.session).push({ role: 'command', content: `Runtime error: ${outcome.message}` });
|
|
36
|
+
props.addLog(`runtime error: ${outcome.message}`);
|
|
37
|
+
}
|
|
30
38
|
props.refresh();
|
|
31
39
|
return { exit: false, runtime: true };
|
|
32
40
|
}
|
package/src/shell/useSession.ts
CHANGED
|
@@ -169,6 +169,18 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
169
169
|
let lastVisiblePlan: Array<{ step: number; description: string; status: string }> | null = null;
|
|
170
170
|
const activities = createMemo(() => {
|
|
171
171
|
version();
|
|
172
|
+
const workflowActivities = nonEmptyRuntimeArray(runtimeState()?.workflow?.nodes?.filter((node: any) => node.type === 'activity'));
|
|
173
|
+
if (workflowActivities) {
|
|
174
|
+
return workflowActivities.map((node: any) => ({
|
|
175
|
+
...(node.raw ?? {}),
|
|
176
|
+
key: node.key,
|
|
177
|
+
label: node.label,
|
|
178
|
+
status: node.status,
|
|
179
|
+
terminal: ['done', 'failed', 'cancelled'].includes(String(node.status)),
|
|
180
|
+
_runtime: true,
|
|
181
|
+
_workflow: true,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
172
184
|
const runtimeActivities = nonEmptyRuntimeArray(runtimeState()?.activities);
|
|
173
185
|
if (runtimeActivities) {
|
|
174
186
|
return runtimeActivities.map((activity: any) => ({ ...activity, _runtime: true }));
|
|
@@ -184,6 +196,10 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
184
196
|
});
|
|
185
197
|
const queueItems = createMemo(() => {
|
|
186
198
|
version();
|
|
199
|
+
const workflowQueue = nonEmptyRuntimeArray(runtimeState()?.workflow?.nodes?.filter((node: any) => node.type === 'queue'));
|
|
200
|
+
if (workflowQueue) {
|
|
201
|
+
return workflowQueue.map((node: any) => ({ ...(node.raw ?? {}), id: node.itemId ?? node.id, label: node.label, status: node.status, _runtime: true, _workflow: true }));
|
|
202
|
+
}
|
|
187
203
|
const runtimeQueue = nonEmptyRuntimeArray(runtimeState()?.queue);
|
|
188
204
|
if (runtimeQueue) return runtimeQueue.map((item: any) => ({ ...item, _runtime: true }));
|
|
189
205
|
return projectQueue((session as any).headlessPlan, (session as any).jobQueue ?? [], { workspace: (session as any).workspace ?? null })
|
|
@@ -191,6 +207,14 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
191
207
|
});
|
|
192
208
|
const queueInfo = createMemo(() => {
|
|
193
209
|
version();
|
|
210
|
+
const workflowQueue = runtimeState()?.workflow?.nodes?.filter((node: any) => node.type === 'queue');
|
|
211
|
+
if (Array.isArray(workflowQueue)) {
|
|
212
|
+
return {
|
|
213
|
+
active: workflowQueue.filter((item: any) => ['waiting', 'starting', 'running', 'queued', 'pending', 'pending_approval'].includes(String(item.status ?? '').toLowerCase())).length,
|
|
214
|
+
current: workflowQueue.filter((item: any) => ['starting', 'running'].includes(String(item.status ?? '').toLowerCase())).length,
|
|
215
|
+
frozen: 0,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
194
218
|
const runtimeQueue = runtimeState()?.queue;
|
|
195
219
|
if (Array.isArray(runtimeQueue)) {
|
|
196
220
|
return {
|
|
@@ -203,6 +227,16 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
203
227
|
});
|
|
204
228
|
const plan = createMemo(() => {
|
|
205
229
|
version();
|
|
230
|
+
const workflowTasks = nonEmptyRuntimeArray(runtimeState()?.workflow?.nodes?.filter((node: any) => node.type === 'task'));
|
|
231
|
+
if (workflowTasks) {
|
|
232
|
+
return workflowTasks
|
|
233
|
+
.sort((a: any, b: any) => Number(a.step ?? 0) - Number(b.step ?? 0))
|
|
234
|
+
.map((node: any, index: number) => ({
|
|
235
|
+
step: Number(node.step ?? index + 1),
|
|
236
|
+
description: String(node.description ?? node.label ?? `Step ${index + 1}`),
|
|
237
|
+
status: String(node.status ?? 'pending'),
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
206
240
|
const runtimePlan = nonEmptyRuntimeArray(runtimeState()?.plan);
|
|
207
241
|
if (runtimePlan) {
|
|
208
242
|
return runtimePlan.map((step: any, index: number) => ({
|