@dotdrelle/wiki-manager 0.6.17
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/.env.example +58 -0
- package/LICENSE +108 -0
- package/README.md +693 -0
- package/agents.docker-compose.yml +96 -0
- package/bin/wiki-manager.js +34 -0
- package/bunfig.toml +1 -0
- package/docker-compose.yml +135 -0
- package/mcp.endpoints.example.json +28 -0
- package/package.json +57 -0
- package/src/agent/graph.js +638 -0
- package/src/agent/llm.js +239 -0
- package/src/cli/wiki-manager.js +389 -0
- package/src/commands/slash.js +1044 -0
- package/src/core/activity.js +236 -0
- package/src/core/activity.test.js +127 -0
- package/src/core/agentEvents.js +238 -0
- package/src/core/agentEvents.test.js +134 -0
- package/src/core/compose.js +310 -0
- package/src/core/documentIntake.js +311 -0
- package/src/core/documentIntake.test.js +121 -0
- package/src/core/env.js +57 -0
- package/src/core/jobQueue.js +197 -0
- package/src/core/mcp.js +402 -0
- package/src/core/mcp.test.js +228 -0
- package/src/core/plan.js +181 -0
- package/src/core/plan.test.js +168 -0
- package/src/core/skills.js +142 -0
- package/src/core/wikirc.js +65 -0
- package/src/core/workspaces.js +81 -0
- package/src/shell/FileEditorDialog.tsx +94 -0
- package/src/shell/LeftPane.tsx +680 -0
- package/src/shell/RightPane.tsx +291 -0
- package/src/shell/SlashDialog.tsx +39 -0
- package/src/shell/renderer.ts +69 -0
- package/src/shell/repl.js +1490 -0
- package/src/shell/tui.tsx +205 -0
- package/src/shell/useAgent.ts +47 -0
- package/src/shell/useSession.ts +370 -0
- package/tsconfig.json +16 -0
- package/wiki-workspace +773 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { render, useKeyboard, useRenderer, useSelectionHandler, useTerminalDimensions } from '@opentui/solid';
|
|
4
|
+
import { createMemo, createSignal, onCleanup } from 'solid-js';
|
|
5
|
+
import { FileEditorDialog } from './FileEditorDialog';
|
|
6
|
+
import { LeftPane } from './LeftPane';
|
|
7
|
+
import { RightPane } from './RightPane';
|
|
8
|
+
import { SlashDialog } from './SlashDialog';
|
|
9
|
+
import { useSession } from './useSession';
|
|
10
|
+
|
|
11
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
12
|
+
|
|
13
|
+
function copyToClipboard(text: string, renderer: unknown) {
|
|
14
|
+
try {
|
|
15
|
+
if ((renderer as any).copyToClipboardOSC52?.(text)) return true;
|
|
16
|
+
} catch {
|
|
17
|
+
// Fall through to platform clipboard tools.
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
if (process.platform === 'darwin') {
|
|
21
|
+
execFileSync('pbcopy', [], { input: text });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (process.platform === 'win32') {
|
|
25
|
+
execFileSync('clip', [], { input: text });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
execFileSync('wl-copy', [], { input: text });
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
execFileSync('xclip', ['-selection', 'clipboard'], { input: text });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function App(props: { agent: unknown; packageJson: Record<string, unknown> }) {
|
|
41
|
+
const renderer = useRenderer();
|
|
42
|
+
const dimensions = useTerminalDimensions();
|
|
43
|
+
const [spinnerIndex, setSpinnerIndex] = createSignal(0);
|
|
44
|
+
const [exitHint, setExitHint] = createSignal(false);
|
|
45
|
+
const [copyHint, setCopyHint] = createSignal<string | null>(null);
|
|
46
|
+
const [chatInputHeight, setChatInputHeight] = createSignal(3);
|
|
47
|
+
let ctrlCTimer: ReturnType<typeof setTimeout> | null = null;
|
|
48
|
+
let copyHintTimer: ReturnType<typeof setTimeout> | null = null;
|
|
49
|
+
let selectionCopyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
50
|
+
let lastCopiedSelection = '';
|
|
51
|
+
const state = useSession(props);
|
|
52
|
+
const conversationRows = createMemo(() => Math.max(4, dimensions().height - 5 - chatInputHeight()));
|
|
53
|
+
const rightColumns = createMemo(() => {
|
|
54
|
+
const width = dimensions().width;
|
|
55
|
+
return Math.max(26, Math.min(44, Math.floor(width * 0.32)));
|
|
56
|
+
});
|
|
57
|
+
const leftColumns = createMemo(() => Math.max(32, dimensions().width - rightColumns() - 1));
|
|
58
|
+
const conversationColumns = createMemo(() => {
|
|
59
|
+
return Math.max(24, leftColumns() - 4);
|
|
60
|
+
});
|
|
61
|
+
const submit = (value?: string) => {
|
|
62
|
+
if (state.slash()) {
|
|
63
|
+
state.completeSelected();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
void state.submitInput(value).then((result) => {
|
|
67
|
+
if (result?.exit) renderer.destroy();
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const showCopyHint = (message: string) => {
|
|
72
|
+
setCopyHint(message);
|
|
73
|
+
if (copyHintTimer) clearTimeout(copyHintTimer);
|
|
74
|
+
copyHintTimer = setTimeout(() => { setCopyHint(null); copyHintTimer = null; }, 1400);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
useSelectionHandler((selection: any) => {
|
|
78
|
+
const text = String(selection?.getSelectedText?.() ?? '').trimEnd();
|
|
79
|
+
if (!text.trim()) return;
|
|
80
|
+
if (selectionCopyTimer) clearTimeout(selectionCopyTimer);
|
|
81
|
+
selectionCopyTimer = setTimeout(() => {
|
|
82
|
+
selectionCopyTimer = null;
|
|
83
|
+
if (text === lastCopiedSelection) return;
|
|
84
|
+
lastCopiedSelection = text;
|
|
85
|
+
showCopyHint(copyToClipboard(text, renderer) ? 'Selection copied.' : 'Selection ready. Use terminal copy.');
|
|
86
|
+
}, selection?.isDragging ? 700 : 80);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
onCleanup(() => {
|
|
90
|
+
if (copyHintTimer) clearTimeout(copyHintTimer);
|
|
91
|
+
if (selectionCopyTimer) clearTimeout(selectionCopyTimer);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const spinnerTimer = setInterval(() => {
|
|
95
|
+
if (state.busy()) setSpinnerIndex((value) => (value + 1) % 10);
|
|
96
|
+
}, 90);
|
|
97
|
+
onCleanup(() => {
|
|
98
|
+
clearInterval(spinnerTimer);
|
|
99
|
+
if (ctrlCTimer) clearTimeout(ctrlCTimer);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
useKeyboard((key) => {
|
|
103
|
+
if (state.activeEditor()) {
|
|
104
|
+
if (key.name === 'escape') state.closeEditor();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (key.ctrl && key.name === 'c') {
|
|
108
|
+
if (state.busy()) {
|
|
109
|
+
state.abort();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (exitHint()) {
|
|
113
|
+
renderer.destroy();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
setExitHint(true);
|
|
117
|
+
if (ctrlCTimer) clearTimeout(ctrlCTimer);
|
|
118
|
+
ctrlCTimer = setTimeout(() => {
|
|
119
|
+
setExitHint(false);
|
|
120
|
+
ctrlCTimer = null;
|
|
121
|
+
}, 1600);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (key.ctrl && key.name === 'q') {
|
|
125
|
+
state.toggleRightTab();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (state.busy()) return;
|
|
129
|
+
if (key.name === 'tab') state.completeSelected();
|
|
130
|
+
if (key.name === 'pageup') state.scrollConversation(conversationRows());
|
|
131
|
+
else if (key.name === 'pagedown') state.scrollConversation(-conversationRows());
|
|
132
|
+
if (key.name === 'up' && state.slash()) state.moveCompletion(-1);
|
|
133
|
+
else if (key.name === 'down' && state.slash()) state.moveCompletion(1);
|
|
134
|
+
else if (key.name === 'up' && !state.input().includes('\n')) state.historyUp();
|
|
135
|
+
else if (key.name === 'down' && !state.input().includes('\n')) state.historyDown();
|
|
136
|
+
else if (key.name === 'escape') {
|
|
137
|
+
if (state.slash()) state.dismissSlash();
|
|
138
|
+
else state.setInput('');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const hintLine = () => {
|
|
143
|
+
if (copyHint()) return copyHint();
|
|
144
|
+
if (exitHint()) return 'Press Ctrl+C again to exit.';
|
|
145
|
+
return null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<box width="100%" height="100%" flexDirection="row">
|
|
150
|
+
<LeftPane
|
|
151
|
+
width={leftColumns()}
|
|
152
|
+
title={state.title()}
|
|
153
|
+
statusLine={state.statusLine()}
|
|
154
|
+
hintLine={hintLine()}
|
|
155
|
+
showWelcome={state.showWelcome()}
|
|
156
|
+
messages={state.messages()}
|
|
157
|
+
prompt={state.prompt()}
|
|
158
|
+
input={state.input()}
|
|
159
|
+
busy={state.busy()}
|
|
160
|
+
chatMode={state.chatMode()}
|
|
161
|
+
chatFocused={!state.activeEditor()}
|
|
162
|
+
setInput={state.setInput}
|
|
163
|
+
submit={submit}
|
|
164
|
+
conversationRows={conversationRows()}
|
|
165
|
+
conversationColumns={conversationColumns()}
|
|
166
|
+
conversationScroll={state.conversationScroll()}
|
|
167
|
+
scrollConversation={state.scrollConversation}
|
|
168
|
+
spinnerFrame={SPINNER_FRAMES[spinnerIndex()] ?? SPINNER_FRAMES[0]}
|
|
169
|
+
onInputHeightChange={setChatInputHeight}
|
|
170
|
+
onCopy={(content) => showCopyHint(copyToClipboard(content, renderer) ? 'Copied.' : 'Copy failed.')}
|
|
171
|
+
/>
|
|
172
|
+
<box width={1} height="100%" flexDirection="column">
|
|
173
|
+
{Array.from({ length: dimensions().height }, () => (
|
|
174
|
+
<text fg="#4B5563">│</text>
|
|
175
|
+
))}
|
|
176
|
+
</box>
|
|
177
|
+
<RightPane
|
|
178
|
+
width={rightColumns()}
|
|
179
|
+
activities={state.activities()}
|
|
180
|
+
logs={state.logs()}
|
|
181
|
+
plan={state.plan()}
|
|
182
|
+
queueItems={state.queueItems()}
|
|
183
|
+
queueInfo={state.queueInfo()}
|
|
184
|
+
activeTab={state.rightTab()}
|
|
185
|
+
onTabClick={state.selectRightTab}
|
|
186
|
+
/>
|
|
187
|
+
<SlashDialog context={state.activeEditor() ? null : state.slash()} />
|
|
188
|
+
<FileEditorDialog
|
|
189
|
+
editor={state.activeEditor()}
|
|
190
|
+
width={dimensions().width}
|
|
191
|
+
height={dimensions().height}
|
|
192
|
+
onSave={state.saveEditor}
|
|
193
|
+
onCancel={state.closeEditor}
|
|
194
|
+
/>
|
|
195
|
+
</box>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function runOpenTuiShell({ agent, packageJson }: { agent: unknown; packageJson: Record<string, unknown> }) {
|
|
200
|
+
await render(() => <App agent={agent} packageJson={packageJson} />, {
|
|
201
|
+
exitOnCtrlC: false,
|
|
202
|
+
useMouse: true,
|
|
203
|
+
targetFps: 30,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createSignal } from 'solid-js';
|
|
2
|
+
import { runLine } from './repl.js';
|
|
3
|
+
|
|
4
|
+
export function useAgent(props: { agent: unknown; packageJson: Record<string, unknown>; session: Record<string, any>; chatMode: () => boolean; refresh: () => void; addLog: (line: string) => void }) {
|
|
5
|
+
const [busy, setBusy] = createSignal(false);
|
|
6
|
+
const [abortController, setAbortController] = createSignal<AbortController | null>(null);
|
|
7
|
+
|
|
8
|
+
async function submit(line: string) {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (!trimmed) return { exit: false };
|
|
11
|
+
if (busy()) return { exit: false, busy: true };
|
|
12
|
+
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
setAbortController(controller);
|
|
15
|
+
props.session._abortSignal = controller.signal;
|
|
16
|
+
setBusy(true);
|
|
17
|
+
props.addLog(`input: ${trimmed}`);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const result = await runLine(trimmed, {
|
|
21
|
+
agent: props.agent,
|
|
22
|
+
packageJson: props.packageJson,
|
|
23
|
+
session: props.session,
|
|
24
|
+
onUpdate: props.refresh,
|
|
25
|
+
onStep: props.addLog,
|
|
26
|
+
chatMode: props.chatMode(),
|
|
27
|
+
});
|
|
28
|
+
props.refresh();
|
|
29
|
+
return result;
|
|
30
|
+
} catch (err: any) {
|
|
31
|
+
if (err?.name === 'AbortError') return { exit: false, aborted: true };
|
|
32
|
+
props.addLog(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
33
|
+
return { exit: false };
|
|
34
|
+
} finally {
|
|
35
|
+
delete props.session._abortSignal;
|
|
36
|
+
setAbortController(null);
|
|
37
|
+
setBusy(false);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function abort() {
|
|
42
|
+
abortController()?.abort();
|
|
43
|
+
props.addLog('interrupt requested');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { busy, submit, abort };
|
|
47
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { createMemo, createSignal, onCleanup } from 'solid-js';
|
|
4
|
+
import { formatMcpToolResult, callMcpTool } from '../core/mcp.js';
|
|
5
|
+
import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
6
|
+
import { formatPlanStatus, formatCompletedActivities } from '../core/plan.js';
|
|
7
|
+
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
8
|
+
import { queueCounts, startNextQueuedJob, syncQueueWithActivity } from '../core/jobQueue.js';
|
|
9
|
+
import type { ActiveFileEditor } from './FileEditorDialog';
|
|
10
|
+
import {
|
|
11
|
+
completionContext,
|
|
12
|
+
completionDescription,
|
|
13
|
+
conversationMessages,
|
|
14
|
+
createSession,
|
|
15
|
+
} from './repl.js';
|
|
16
|
+
import { useAgent } from './useAgent';
|
|
17
|
+
|
|
18
|
+
function buildContinuationPrompt(session: any, completed: any[]): string {
|
|
19
|
+
const originalTask = [...conversationMessages(session)]
|
|
20
|
+
.reverse()
|
|
21
|
+
.find((message: any) => message.role === 'user'
|
|
22
|
+
&& !String(message.content ?? '').startsWith('Completed activities:')
|
|
23
|
+
&& !String(message.content ?? '').startsWith('Original task:'))?.content;
|
|
24
|
+
return [
|
|
25
|
+
originalTask ? `Original task:\n${originalTask}\n` : null,
|
|
26
|
+
'Completed activities:',
|
|
27
|
+
formatCompletedActivities(completed) || '(none)',
|
|
28
|
+
session.headlessPlan ? `\nPlan status:\n${formatPlanStatus(session.headlessPlan)}` : null,
|
|
29
|
+
'\nContinue the plan. Start the next pending step only.',
|
|
30
|
+
'If all steps are complete, provide a final summary.',
|
|
31
|
+
].filter(Boolean).join('\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useSession(props: { agent: unknown; packageJson: Record<string, unknown> }) {
|
|
35
|
+
const session = createSession();
|
|
36
|
+
const [version, setVersion] = createSignal(0);
|
|
37
|
+
const [logs, setLogs] = createSignal<string[]>([]);
|
|
38
|
+
const [input, setInput] = createSignal('');
|
|
39
|
+
const [chatMode, setChatMode] = createSignal(false);
|
|
40
|
+
(session as any).chatMode = false;
|
|
41
|
+
const [dismissedSlashInput, setDismissedSlashInput] = createSignal<string | null>(null);
|
|
42
|
+
const [history, setHistory] = createSignal<string[]>([]);
|
|
43
|
+
const [historyIndex, setHistoryIndex] = createSignal<number | null>(null);
|
|
44
|
+
const [selectedCompletion, setSelectedCompletion] = createSignal(0);
|
|
45
|
+
const [conversationScroll, setConversationScroll] = createSignal(0);
|
|
46
|
+
const [showWelcome, setShowWelcome] = createSignal(true);
|
|
47
|
+
const [activeEditor, setActiveEditor] = createSignal<ActiveFileEditor | null>(null);
|
|
48
|
+
const [rightTab, setRightTab] = createSignal<'plan' | 'queue'>('plan');
|
|
49
|
+
const pollBusy = new Set<string>();
|
|
50
|
+
const matchedActivityKeys = new Set<string>();
|
|
51
|
+
const lastActivityLines = new Map<string, string>();
|
|
52
|
+
const lastActivityDetails = new Map<string, string>();
|
|
53
|
+
|
|
54
|
+
const refresh = () => setVersion((value) => value + 1);
|
|
55
|
+
(session as any)._onPlanUpdate = refresh;
|
|
56
|
+
(session as any)._onOpenEditor = (editor: ActiveFileEditor) => {
|
|
57
|
+
setShowWelcome(false);
|
|
58
|
+
setActiveEditor(editor);
|
|
59
|
+
};
|
|
60
|
+
const addLog = (line: string) => {
|
|
61
|
+
setLogs((items) => [...items, `${new Date().toLocaleTimeString()} ${line}`].slice(-200));
|
|
62
|
+
};
|
|
63
|
+
const agent = useAgent({ agent: props.agent, packageJson: props.packageJson, session, chatMode, refresh, addLog });
|
|
64
|
+
|
|
65
|
+
const messages = createMemo(() => {
|
|
66
|
+
version();
|
|
67
|
+
return [...conversationMessages(session)];
|
|
68
|
+
});
|
|
69
|
+
const prompt = createMemo(() => {
|
|
70
|
+
version();
|
|
71
|
+
return chatMode() ? '[chat] › ' : '[agent] › ';
|
|
72
|
+
});
|
|
73
|
+
const title = createMemo(() => {
|
|
74
|
+
version();
|
|
75
|
+
const workspace = session.workspace ?? 'myspace';
|
|
76
|
+
const profile = session.wikirc?.profile ?? 'donna';
|
|
77
|
+
return `${workspace} > ${profile}`;
|
|
78
|
+
});
|
|
79
|
+
const statusLine = createMemo(() => {
|
|
80
|
+
version();
|
|
81
|
+
return [
|
|
82
|
+
`wiki-manager ${props.packageJson.version ?? ''}`.trim(),
|
|
83
|
+
session.workspace ? session.workspace : 'no workspace',
|
|
84
|
+
session.wikirc?.profile ? session.wikirc.profile : 'no wikirc',
|
|
85
|
+
session.language ? session.language : 'no language',
|
|
86
|
+
session.llm ? 'llm ready' : 'llm limited',
|
|
87
|
+
].join(' ');
|
|
88
|
+
});
|
|
89
|
+
const slash = createMemo(() => {
|
|
90
|
+
if (input() === dismissedSlashInput()) return null;
|
|
91
|
+
const context = completionContext(input(), session);
|
|
92
|
+
if (!context) return null;
|
|
93
|
+
return {
|
|
94
|
+
...context,
|
|
95
|
+
selected: Math.min(selectedCompletion(), Math.max(0, context.matches.length - 1)),
|
|
96
|
+
items: context.matches.slice(0, 10).map((value: string) => ({
|
|
97
|
+
value,
|
|
98
|
+
description: completionDescription(value, context.parts),
|
|
99
|
+
})),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
const mcpServers = createMemo(() => {
|
|
103
|
+
version();
|
|
104
|
+
return Object.entries(session.mcp ?? {}).map(([name, value]: [string, any]) => ({
|
|
105
|
+
name,
|
|
106
|
+
status: value?.status ?? 'missing',
|
|
107
|
+
detail: value?.detail ?? '',
|
|
108
|
+
}));
|
|
109
|
+
});
|
|
110
|
+
let lastVisibleActivities: any[] = [];
|
|
111
|
+
let lastVisiblePlan: Array<{ step: number; description: string; status: string }> | null = null;
|
|
112
|
+
const activities = createMemo(() => {
|
|
113
|
+
version();
|
|
114
|
+
const current = sessionActivities(session);
|
|
115
|
+
if (current.length > 0) {
|
|
116
|
+
lastVisibleActivities = current.map((activity) => ({ ...activity }));
|
|
117
|
+
return current;
|
|
118
|
+
}
|
|
119
|
+
return agent.busy() && lastVisibleActivities.length > 0
|
|
120
|
+
? lastVisibleActivities.map((activity) => ({ ...activity }))
|
|
121
|
+
: current;
|
|
122
|
+
});
|
|
123
|
+
const queueItems = createMemo(() => {
|
|
124
|
+
version();
|
|
125
|
+
return ((session as any).jobQueue ?? []).map((item: any) => ({ ...item }));
|
|
126
|
+
});
|
|
127
|
+
const queueInfo = createMemo(() => {
|
|
128
|
+
version();
|
|
129
|
+
return queueCounts(session);
|
|
130
|
+
});
|
|
131
|
+
const plan = createMemo(() => {
|
|
132
|
+
version();
|
|
133
|
+
const p = (session as any).headlessPlan as Array<{ step: number; description: string; status: string }> | null;
|
|
134
|
+
const current = p ? p.map((s) => ({ ...s })) : null;
|
|
135
|
+
if (current && current.length > 0) {
|
|
136
|
+
lastVisiblePlan = current.map((step) => ({ ...step }));
|
|
137
|
+
return current;
|
|
138
|
+
}
|
|
139
|
+
return agent.busy() && lastVisiblePlan && lastVisiblePlan.length > 0
|
|
140
|
+
? lastVisiblePlan.map((step) => ({ ...step }))
|
|
141
|
+
: current;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const activityPollTimer = setInterval(() => {
|
|
145
|
+
for (const activity of sessionActivities(session)) {
|
|
146
|
+
if (activity.terminal || !activity.poll) continue;
|
|
147
|
+
const key = activity.key ?? `${activity.poll.server}:${activity.id ?? activity.label}`;
|
|
148
|
+
if (pollBusy.has(key)) continue;
|
|
149
|
+
const endpoint = (session.mcp as any)?.[activity.poll.server];
|
|
150
|
+
if (!endpoint || endpoint.status !== 'connected') continue;
|
|
151
|
+
const intervalMs = activity.poll.intervalMs ?? 2500;
|
|
152
|
+
const lastPolledAt = Date.parse((activity as any).lastPolledAt ?? '0');
|
|
153
|
+
if (Date.now() - lastPolledAt < intervalMs) continue;
|
|
154
|
+
pollBusy.add(key);
|
|
155
|
+
(activity as any).lastPolledAt = new Date().toISOString();
|
|
156
|
+
void callMcpTool(session.mcp, activity.poll.server, activity.poll.tool, activity.poll.args ?? {})
|
|
157
|
+
.then((result) => {
|
|
158
|
+
const payload = parseJsonText(formatMcpToolResult(result));
|
|
159
|
+
const polledActivity = extractActivity(payload, {
|
|
160
|
+
server: activity.poll.server,
|
|
161
|
+
tool: activity.poll.tool,
|
|
162
|
+
});
|
|
163
|
+
if (polledActivity) {
|
|
164
|
+
dispatchAgentEvent(session, createAgentEvent('activity_upserted', {
|
|
165
|
+
origin: 'poll',
|
|
166
|
+
payload: { activity: polledActivity },
|
|
167
|
+
}));
|
|
168
|
+
syncQueueWithActivity(session, polledActivity);
|
|
169
|
+
refresh();
|
|
170
|
+
const updated = sessionActivities(session).find((a) => a.key === key);
|
|
171
|
+
if (updated) {
|
|
172
|
+
const line = `${updated.label ?? key} -> ${updated.status}${updated.error ? ` (${updated.error})` : ''}`;
|
|
173
|
+
if (lastActivityLines.get(key) !== line) {
|
|
174
|
+
lastActivityLines.set(key, line);
|
|
175
|
+
addLog(`activity: ${line}`);
|
|
176
|
+
}
|
|
177
|
+
const detail = (updated as any).progress?.detail ?? null;
|
|
178
|
+
if (detail && lastActivityDetails.get(key) !== detail) {
|
|
179
|
+
lastActivityDetails.set(key, detail);
|
|
180
|
+
const shortLabel = (updated as any).progress?.label ?? updated.label ?? key;
|
|
181
|
+
addLog(`${shortLabel}: ${detail}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (updated?.terminal && !matchedActivityKeys.has(key)) {
|
|
185
|
+
matchedActivityKeys.add(key);
|
|
186
|
+
void startNextQueuedJob(session, { addLog, refresh });
|
|
187
|
+
const plan = (session as any).headlessPlan;
|
|
188
|
+
const stillRunning = sessionActivities(session).filter((a) => !a.terminal && a.poll);
|
|
189
|
+
const pendingSteps = (plan ?? []).filter((s: any) => s.status === 'pending');
|
|
190
|
+
if (stillRunning.length === 0 && pendingSteps.length > 0 && !agent.busy()) {
|
|
191
|
+
const completedAll = sessionActivities(session).filter((a) => a.terminal);
|
|
192
|
+
const prompt = buildContinuationPrompt(session, completedAll);
|
|
193
|
+
void agent.submit(prompt);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
.catch((err) => {
|
|
199
|
+
addLog(`activity poll error: ${err instanceof Error ? err.message : String(err)}`);
|
|
200
|
+
})
|
|
201
|
+
.finally(() => {
|
|
202
|
+
pollBusy.delete(key);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}, 1000);
|
|
206
|
+
onCleanup(() => clearInterval(activityPollTimer));
|
|
207
|
+
|
|
208
|
+
const queueFallbackTimer = setInterval(() => {
|
|
209
|
+
if ((session as any).jobQueue?.some((item: any) => item.status === 'waiting')) {
|
|
210
|
+
void startNextQueuedJob(session, { addLog, refresh });
|
|
211
|
+
}
|
|
212
|
+
}, 10000);
|
|
213
|
+
onCleanup(() => clearInterval(queueFallbackTimer));
|
|
214
|
+
|
|
215
|
+
async function submitInput(submittedValue?: string) {
|
|
216
|
+
const line = typeof submittedValue === 'string' && submittedValue.trim() ? submittedValue : input();
|
|
217
|
+
if (agent.busy()) return { exit: false, busy: true };
|
|
218
|
+
setInput('');
|
|
219
|
+
setConversationScroll(0);
|
|
220
|
+
if (line.trim()) {
|
|
221
|
+
setShowWelcome(false);
|
|
222
|
+
setHistory((items) => [...items, line].slice(-100));
|
|
223
|
+
}
|
|
224
|
+
setHistoryIndex(null);
|
|
225
|
+
matchedActivityKeys.clear();
|
|
226
|
+
lastActivityLines.clear();
|
|
227
|
+
lastActivityDetails.clear();
|
|
228
|
+
const result = await agent.submit(line);
|
|
229
|
+
if ((result as any)?.setMode === 'chat') {
|
|
230
|
+
setChatMode(true);
|
|
231
|
+
(session as any).chatMode = true;
|
|
232
|
+
refresh();
|
|
233
|
+
} else if ((result as any)?.setMode === 'agent') {
|
|
234
|
+
setChatMode(false);
|
|
235
|
+
(session as any).chatMode = false;
|
|
236
|
+
refresh();
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function updateInput(value: string) {
|
|
242
|
+
setInput(value);
|
|
243
|
+
if (value !== dismissedSlashInput()) setDismissedSlashInput(null);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function dismissSlash() {
|
|
247
|
+
if (slash()) setDismissedSlashInput(input());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function completeSelected() {
|
|
251
|
+
const context = slash();
|
|
252
|
+
if (!context || context.items.length === 0) return;
|
|
253
|
+
const selected = context.items[context.selected]?.value;
|
|
254
|
+
if (!selected) return;
|
|
255
|
+
const lastSpace = input().lastIndexOf(' ');
|
|
256
|
+
const base = input().endsWith(' ') ? input() : input().slice(0, lastSpace + 1);
|
|
257
|
+
setInput(`${base}${selected} `);
|
|
258
|
+
setSelectedCompletion(0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function moveCompletion(delta: number) {
|
|
262
|
+
const count = slash()?.items.length ?? 0;
|
|
263
|
+
if (!count) return;
|
|
264
|
+
setSelectedCompletion((value) => (value + delta + count) % count);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function historyUp() {
|
|
268
|
+
const items = history();
|
|
269
|
+
if (!items.length) return;
|
|
270
|
+
const next = historyIndex() === null ? items.length - 1 : Math.max(0, historyIndex()! - 1);
|
|
271
|
+
setHistoryIndex(next);
|
|
272
|
+
setInput(items[next] ?? '');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function historyDown() {
|
|
276
|
+
const items = history();
|
|
277
|
+
const current = historyIndex();
|
|
278
|
+
if (current === null) return;
|
|
279
|
+
const next = current + 1;
|
|
280
|
+
if (next >= items.length) {
|
|
281
|
+
setHistoryIndex(null);
|
|
282
|
+
setInput('');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
setHistoryIndex(next);
|
|
286
|
+
setInput(items[next] ?? '');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function scrollConversation(delta: number) {
|
|
290
|
+
setConversationScroll((value) => Math.max(0, value + delta));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function toggleRightTab() {
|
|
294
|
+
setRightTab((value) => value === 'plan' ? 'queue' : 'plan');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function selectRightTab(tab: 'plan' | 'queue') {
|
|
298
|
+
setRightTab(tab);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function closeEditor() {
|
|
302
|
+
setActiveEditor(null);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function saveEditor(content: string) {
|
|
306
|
+
const editor = activeEditor();
|
|
307
|
+
if (!editor) return { ok: false as const, error: 'No active editor.' };
|
|
308
|
+
if (!session.workspacePath) return { ok: false as const, error: 'No workspace loaded.' };
|
|
309
|
+
const workspaceRoot = resolve(session.workspacePath);
|
|
310
|
+
const targetPath = resolve(editor.filePath);
|
|
311
|
+
const rel = relative(workspaceRoot, targetPath);
|
|
312
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
313
|
+
return { ok: false as const, error: 'Refusing to save outside the workspace.' };
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
writeFileSync(targetPath, content, 'utf8');
|
|
317
|
+
conversationMessages(session).push({ role: 'command', content: `Saved file: ${editor.displayPath}` });
|
|
318
|
+
setActiveEditor(null);
|
|
319
|
+
refresh();
|
|
320
|
+
return { ok: true as const };
|
|
321
|
+
} catch (err) {
|
|
322
|
+
return { ok: false as const, error: err instanceof Error ? err.message : String(err) };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function abort() {
|
|
327
|
+
agent.abort();
|
|
328
|
+
dispatchAgentEvent(session, createAgentEvent('plan_set', {
|
|
329
|
+
origin: 'system',
|
|
330
|
+
payload: { steps: null },
|
|
331
|
+
}));
|
|
332
|
+
matchedActivityKeys.clear();
|
|
333
|
+
refresh();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
session,
|
|
338
|
+
messages,
|
|
339
|
+
logs,
|
|
340
|
+
input,
|
|
341
|
+
setInput: updateInput,
|
|
342
|
+
title,
|
|
343
|
+
statusLine,
|
|
344
|
+
chatMode,
|
|
345
|
+
showWelcome,
|
|
346
|
+
activeEditor,
|
|
347
|
+
prompt,
|
|
348
|
+
slash,
|
|
349
|
+
mcpServers,
|
|
350
|
+
activities,
|
|
351
|
+
queueItems,
|
|
352
|
+
queueInfo,
|
|
353
|
+
rightTab,
|
|
354
|
+
toggleRightTab,
|
|
355
|
+
selectRightTab,
|
|
356
|
+
plan,
|
|
357
|
+
conversationScroll,
|
|
358
|
+
scrollConversation,
|
|
359
|
+
busy: agent.busy,
|
|
360
|
+
abort,
|
|
361
|
+
submitInput,
|
|
362
|
+
completeSelected,
|
|
363
|
+
dismissSlash,
|
|
364
|
+
moveCompletion,
|
|
365
|
+
historyUp,
|
|
366
|
+
historyDown,
|
|
367
|
+
closeEditor,
|
|
368
|
+
saveEditor,
|
|
369
|
+
};
|
|
370
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"jsx": "preserve",
|
|
7
|
+
"jsxImportSource": "@opentui/solid",
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"checkJs": false,
|
|
10
|
+
"strict": false,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"lib": ["ES2022"],
|
|
13
|
+
"types": ["node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"]
|
|
16
|
+
}
|