@mod-computer/cli 0.1.0
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 +125 -0
- package/commands/execute.md +156 -0
- package/commands/overview.md +233 -0
- package/commands/review.md +151 -0
- package/commands/spec.md +169 -0
- package/dist/app.js +227 -0
- package/dist/cli.bundle.js +25824 -0
- package/dist/cli.bundle.js.map +7 -0
- package/dist/cli.js +121 -0
- package/dist/commands/agents-run.js +71 -0
- package/dist/commands/auth.js +151 -0
- package/dist/commands/branch.js +1411 -0
- package/dist/commands/claude-sync.js +772 -0
- package/dist/commands/index.js +43 -0
- package/dist/commands/init.js +378 -0
- package/dist/commands/recover.js +207 -0
- package/dist/commands/spec.js +386 -0
- package/dist/commands/status.js +329 -0
- package/dist/commands/sync.js +95 -0
- package/dist/commands/workspace.js +423 -0
- package/dist/components/conflict-resolution-ui.js +120 -0
- package/dist/components/messages.js +5 -0
- package/dist/components/thread.js +8 -0
- package/dist/config/features.js +72 -0
- package/dist/config/release-profiles/development.json +11 -0
- package/dist/config/release-profiles/mvp.json +12 -0
- package/dist/config/release-profiles/v0.1.json +11 -0
- package/dist/config/release-profiles/v0.2.json +11 -0
- package/dist/containers/branches-container.js +140 -0
- package/dist/containers/directory-container.js +92 -0
- package/dist/containers/thread-container.js +214 -0
- package/dist/containers/threads-container.js +27 -0
- package/dist/containers/workspaces-container.js +27 -0
- package/dist/daemon-worker.js +257 -0
- package/dist/lib/auth-server.js +153 -0
- package/dist/lib/browser.js +35 -0
- package/dist/lib/storage.js +203 -0
- package/dist/services/automatic-file-tracker.js +303 -0
- package/dist/services/cli-orchestrator.js +227 -0
- package/dist/services/feature-flags.js +187 -0
- package/dist/services/file-import-service.js +283 -0
- package/dist/services/file-transformation-service.js +218 -0
- package/dist/services/logger.js +44 -0
- package/dist/services/mod-config.js +61 -0
- package/dist/services/modignore-service.js +326 -0
- package/dist/services/sync-daemon.js +244 -0
- package/dist/services/thread-notification-service.js +50 -0
- package/dist/services/thread-service.js +147 -0
- package/dist/stores/use-directory-store.js +96 -0
- package/dist/stores/use-threads-store.js +46 -0
- package/dist/stores/use-workspaces-store.js +32 -0
- package/dist/types/config.js +16 -0
- package/dist/types/index.js +2 -0
- package/dist/types/workspace-connection.js +2 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": true,
|
|
3
|
+
"workspace-management": true,
|
|
4
|
+
"workspace-branching": true,
|
|
5
|
+
"task-management": false,
|
|
6
|
+
"file-operations": false,
|
|
7
|
+
"agent-integrations": false,
|
|
8
|
+
"sync-operations": false,
|
|
9
|
+
"watch-operations": false,
|
|
10
|
+
"connector-integrations": false
|
|
11
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { readModConfig, writeModConfig } from '../services/mod-config.js';
|
|
5
|
+
import { BranchService } from '@mod/mod-core/services/branch-service';
|
|
6
|
+
import { ThreadService } from '@mod/mod-core/services/thread-service';
|
|
7
|
+
export default function BranchesContainer({ repo, workspace, onSelect }) {
|
|
8
|
+
const [threads, setThreads] = useState([]);
|
|
9
|
+
const [idx, setIdx] = useState(0);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
const desiredBranchRef = useRef(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
try {
|
|
14
|
+
desiredBranchRef.current = readModConfig()?.activeBranchId || null;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
desiredBranchRef.current = null;
|
|
18
|
+
}
|
|
19
|
+
}, []);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let cancelled = false;
|
|
22
|
+
let subHandle = null;
|
|
23
|
+
(async () => {
|
|
24
|
+
try {
|
|
25
|
+
if (!workspace)
|
|
26
|
+
return;
|
|
27
|
+
const wsHandle = await repo.find(workspace.id);
|
|
28
|
+
let wsDoc = await wsHandle.doc();
|
|
29
|
+
if (!wsDoc && !cancelled) {
|
|
30
|
+
const deadline = Date.now() + 5000;
|
|
31
|
+
while (!wsDoc && Date.now() < deadline) {
|
|
32
|
+
try {
|
|
33
|
+
wsDoc = await wsHandle.doc();
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
if (wsDoc)
|
|
37
|
+
break;
|
|
38
|
+
await new Promise(r => setTimeout(r, 120));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const branchesDocId = wsDoc?.branchesDocId;
|
|
42
|
+
const buildList = async () => {
|
|
43
|
+
try {
|
|
44
|
+
const branchService = new BranchService(repo);
|
|
45
|
+
const branches = await branchService.getBranchesForWorkspace(branchesDocId);
|
|
46
|
+
const list = [];
|
|
47
|
+
for (const b of (branches || [])) {
|
|
48
|
+
const bId = String(b?.id || '');
|
|
49
|
+
if (!bId)
|
|
50
|
+
continue;
|
|
51
|
+
const tId = b?.threadId ? String(b.threadId) : bId;
|
|
52
|
+
list.push({
|
|
53
|
+
id: tId,
|
|
54
|
+
name: b?.name || 'Thread',
|
|
55
|
+
branchId: bId,
|
|
56
|
+
hasThread: Boolean(b?.threadId),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (!cancelled)
|
|
60
|
+
setThreads(list);
|
|
61
|
+
if (!cancelled && desiredBranchRef.current) {
|
|
62
|
+
const matchIdx = list.findIndex(t => t.branchId === desiredBranchRef.current);
|
|
63
|
+
if (matchIdx >= 0) {
|
|
64
|
+
setIdx(matchIdx);
|
|
65
|
+
desiredBranchRef.current = null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
if (!cancelled)
|
|
71
|
+
setError(e?.message || 'Failed to load threads');
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
if (!branchesDocId) {
|
|
75
|
+
if (!cancelled)
|
|
76
|
+
setThreads([]);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Subscribe to branchesDoc hydration and rebuild on change
|
|
80
|
+
try {
|
|
81
|
+
subHandle = await repo.find(branchesDocId);
|
|
82
|
+
const onChange = async () => { await buildList(); };
|
|
83
|
+
try {
|
|
84
|
+
subHandle.on && subHandle.on('change', onChange);
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
await buildList();
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
if (!cancelled)
|
|
93
|
+
setError(e?.message || 'Failed to load threads');
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
return () => { cancelled = true; try {
|
|
97
|
+
subHandle && subHandle.off && subHandle.off('change');
|
|
98
|
+
}
|
|
99
|
+
catch { } ; };
|
|
100
|
+
}, [repo, workspace]);
|
|
101
|
+
useInput((input, key) => {
|
|
102
|
+
if (key.downArrow)
|
|
103
|
+
setIdx(i => Math.min(i + 1, Math.max(0, threads.length - 1)));
|
|
104
|
+
else if (key.upArrow)
|
|
105
|
+
setIdx(i => Math.max(i - 1, 0));
|
|
106
|
+
else if (key.return) {
|
|
107
|
+
(async () => {
|
|
108
|
+
const t = threads[idx];
|
|
109
|
+
if (!t)
|
|
110
|
+
return;
|
|
111
|
+
let next = t;
|
|
112
|
+
if (t.branchId)
|
|
113
|
+
writeModConfig({ activeBranchId: t.branchId });
|
|
114
|
+
// If id equals branchId, there is no thread yet; create lazily now
|
|
115
|
+
if (t.branchId && String(t.id) === String(t.branchId)) {
|
|
116
|
+
try {
|
|
117
|
+
const wsHandle = await repo.find(workspace.id);
|
|
118
|
+
const threadService = new ThreadService(repo);
|
|
119
|
+
const createdId = await threadService.createThread(t.name || 'New Thread', t.branchId);
|
|
120
|
+
// Persist on branch for branch-first model
|
|
121
|
+
try {
|
|
122
|
+
const bService = new BranchService(repo);
|
|
123
|
+
const wsDoc = await wsHandle.doc();
|
|
124
|
+
if (wsDoc?.branchesDocId) {
|
|
125
|
+
await bService.setThreadId(t.branchId, createdId, wsDoc.branchesDocId);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch { }
|
|
129
|
+
next = { ...t, id: createdId, hasThread: true };
|
|
130
|
+
}
|
|
131
|
+
catch { }
|
|
132
|
+
}
|
|
133
|
+
onSelect(next);
|
|
134
|
+
})();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (error)
|
|
138
|
+
return _jsx(Text, { color: "red", children: error });
|
|
139
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Branches (threads)" }), threads.length === 0 ? (_jsx(Text, { color: "gray", children: "No threads" })) : (threads.map((t, i) => (_jsxs(Text, { color: i === idx ? 'green' : undefined, children: [t.name, ' ', _jsxs(Text, { color: "gray", children: ["(", t.branchId || 'no-branch', ")", t.hasThread === false ? ' — no thread (will create on select)' : ''] })] }, t.id)))), _jsx(Text, { color: "cyan", children: "Enter to select" })] }));
|
|
140
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { useDirectoryStore, useDirectoryLoader } from '../stores/use-directory-store.js';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
export default function DirectoryPageContainer({ repo, selectedWorkspace, selectedThread }) {
|
|
8
|
+
const { loadRootDirectory } = useDirectoryLoader();
|
|
9
|
+
const items = useDirectoryStore((s) => s.items);
|
|
10
|
+
const rootIds = useDirectoryStore((s) => s.rootIds);
|
|
11
|
+
const loading = useDirectoryStore((s) => s.loading);
|
|
12
|
+
const error = useDirectoryStore((s) => s.error);
|
|
13
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
14
|
+
const [status, setStatus] = useState(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let cancelled = false;
|
|
17
|
+
async function loadOnly() {
|
|
18
|
+
loadRootDirectory({ repo, selectedWorkspace, selectedThread });
|
|
19
|
+
}
|
|
20
|
+
loadOnly();
|
|
21
|
+
return () => {
|
|
22
|
+
cancelled = true;
|
|
23
|
+
};
|
|
24
|
+
}, [repo, selectedWorkspace, selectedThread, loadRootDirectory]);
|
|
25
|
+
useInput((input, key) => {
|
|
26
|
+
if (key.downArrow) {
|
|
27
|
+
setSelectedIndex(i => Math.min(i + 1, rootIds.length - 1));
|
|
28
|
+
}
|
|
29
|
+
else if (key.upArrow) {
|
|
30
|
+
setSelectedIndex(i => Math.max(i - 1, 0));
|
|
31
|
+
}
|
|
32
|
+
else if (key.return && key.shift) {
|
|
33
|
+
// TODO: Handle Shift+Enter (expand/collapse folder)
|
|
34
|
+
}
|
|
35
|
+
else if (key.return) {
|
|
36
|
+
// Handle Enter (open file/folder)
|
|
37
|
+
const id = rootIds[selectedIndex];
|
|
38
|
+
const item = items[id];
|
|
39
|
+
if (item && item.type === 'file') {
|
|
40
|
+
openFileInEditor(item).catch(e => setStatus('Failed to open file: ' + e.message));
|
|
41
|
+
}
|
|
42
|
+
// TODO: Handle folder navigation
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
async function openFileInEditor(item) {
|
|
46
|
+
setStatus('Updating working directory...');
|
|
47
|
+
// 1. Fetch file content from Automerge in branch context
|
|
48
|
+
let content = '';
|
|
49
|
+
let fileWrap = null;
|
|
50
|
+
try {
|
|
51
|
+
const { BranchableRepo } = await import('@mod/mod-core/services/branchable-repo');
|
|
52
|
+
const br = new BranchableRepo(repo);
|
|
53
|
+
const branchId = selectedThread?.branchId || selectedThread?.id;
|
|
54
|
+
const workspaceId = selectedWorkspace?.id;
|
|
55
|
+
fileWrap = await br.openHandle(item.id, { branchId, workspaceId });
|
|
56
|
+
const fileDoc = fileWrap.doc();
|
|
57
|
+
content = (fileDoc.content || fileDoc.text || fileDoc.markdown || '');
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Fallback to plain repo handle
|
|
61
|
+
const fh = await repo.find(item.id);
|
|
62
|
+
const d = await fh.doc();
|
|
63
|
+
content = d.content || d.text || '';
|
|
64
|
+
fileWrap = fh;
|
|
65
|
+
}
|
|
66
|
+
// 2. Write to working directory (materialize)
|
|
67
|
+
try {
|
|
68
|
+
const fileName = (item?.name || `${item.id}.txt`);
|
|
69
|
+
const outPath = path.resolve(process.cwd(), fileName);
|
|
70
|
+
// Ensure parent directory exists
|
|
71
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
72
|
+
fs.writeFileSync(outPath, content ?? '', 'utf8');
|
|
73
|
+
setStatus(`Wrote ${fileName} to working directory.`);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
setStatus(`Failed to write file: ${e.message}`);
|
|
77
|
+
}
|
|
78
|
+
setTimeout(() => setStatus(null), 2000);
|
|
79
|
+
}
|
|
80
|
+
if (loading)
|
|
81
|
+
return _jsx(Text, { children: "Loading files..." });
|
|
82
|
+
if (error)
|
|
83
|
+
return _jsx(Text, { color: "red", children: error });
|
|
84
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Files & Folders (root)" }), rootIds.length === 0 ? (_jsx(Text, { color: "gray", children: "No files or folders found." })) : (rootIds.map((id, idx) => {
|
|
85
|
+
const item = items[id];
|
|
86
|
+
if (!item)
|
|
87
|
+
return null;
|
|
88
|
+
// Make key unique by prefixing with type and index
|
|
89
|
+
const key = `${item.type}-${item.id}-${idx}`;
|
|
90
|
+
return (_jsxs(Text, { color: idx === selectedIndex ? 'green' : undefined, children: [item.type === 'folder' ? '\ud83d\udcc1' : '\ud83d\udcc4', " ", item.name] }, key));
|
|
91
|
+
})), status && _jsx(Text, { color: "yellow", children: status })] }));
|
|
92
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
3
|
+
import ThreadView from '../components/thread.js';
|
|
4
|
+
import { CLIThreadService } from '../services/thread-service.js';
|
|
5
|
+
import { log } from '../services/logger.js';
|
|
6
|
+
export default function ThreadPageContainer({ repo, activeThread, workspace, pendingChatInput, onChatInputHandled, handleInputSubmit, }) {
|
|
7
|
+
// Remove input state, only use for agent stream if needed
|
|
8
|
+
const [messages, setMessages] = useState([]);
|
|
9
|
+
const [agentChunks, setAgentChunks] = useState([]);
|
|
10
|
+
const [threadService, setThreadService] = useState(null);
|
|
11
|
+
// Helper to parse bold (**bold**) in content for CLI
|
|
12
|
+
const parseContentSegments = useCallback((content) => {
|
|
13
|
+
const segments = [];
|
|
14
|
+
let currentIndex = 0;
|
|
15
|
+
while (currentIndex < content.length) {
|
|
16
|
+
const boldStart = content.indexOf('**', currentIndex);
|
|
17
|
+
if (boldStart === -1) {
|
|
18
|
+
segments.push({ text: content.slice(currentIndex), isBold: false });
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
if (boldStart > currentIndex) {
|
|
22
|
+
segments.push({ text: content.slice(currentIndex, boldStart), isBold: false });
|
|
23
|
+
}
|
|
24
|
+
const boldEnd = content.indexOf('**', boldStart + 2);
|
|
25
|
+
if (boldEnd === -1) {
|
|
26
|
+
segments.push({ text: content.slice(currentIndex), isBold: false });
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
segments.push({ text: content.slice(boldStart + 2, boldEnd), isBold: true });
|
|
30
|
+
currentIndex = boldEnd + 2;
|
|
31
|
+
}
|
|
32
|
+
return segments;
|
|
33
|
+
}, []);
|
|
34
|
+
// Setup thread service
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
setThreadService(new CLIThreadService(repo));
|
|
37
|
+
}, [repo]);
|
|
38
|
+
// Subscribe to thread changes for real-time updates
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!activeThread || !repo)
|
|
41
|
+
return;
|
|
42
|
+
let cancelled = false;
|
|
43
|
+
let messageUnsubs = [];
|
|
44
|
+
let threadUnsub;
|
|
45
|
+
async function subscribe() {
|
|
46
|
+
const threadId = activeThread.id;
|
|
47
|
+
let tHandle;
|
|
48
|
+
try {
|
|
49
|
+
tHandle = await repo.find(threadId);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
console.warn('[CLI][Thread] thread handle not available yet, will retry on next tick');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
async function updateMessagesAndSubs() {
|
|
56
|
+
if (cancelled)
|
|
57
|
+
return;
|
|
58
|
+
let tDoc;
|
|
59
|
+
try {
|
|
60
|
+
tDoc = tHandle.doc();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const itemIds = Array.isArray(tDoc?.itemIds) ? tDoc.itemIds : [];
|
|
66
|
+
// Unsubscribe previous message listeners
|
|
67
|
+
messageUnsubs.forEach(unsub => unsub());
|
|
68
|
+
// Subscribe to all message docs
|
|
69
|
+
const results = await Promise.allSettled(itemIds.map(id => repo.find(id)));
|
|
70
|
+
const messageHandles = results
|
|
71
|
+
.filter(r => r.status === 'fulfilled')
|
|
72
|
+
.map(r => r.value);
|
|
73
|
+
function updateMessages() {
|
|
74
|
+
if (cancelled)
|
|
75
|
+
return;
|
|
76
|
+
const msgs = [];
|
|
77
|
+
for (const h of messageHandles) {
|
|
78
|
+
try {
|
|
79
|
+
const mDoc = h.doc();
|
|
80
|
+
msgs.push({
|
|
81
|
+
id: mDoc.id,
|
|
82
|
+
text: mDoc.content || mDoc.text || '',
|
|
83
|
+
timestamp: mDoc.timestamp || Date.now(),
|
|
84
|
+
userType: mDoc.userType || 'assistant',
|
|
85
|
+
user: mDoc.user || null,
|
|
86
|
+
_contentHash: (mDoc.content || mDoc.text || '').length,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
}
|
|
91
|
+
setMessages(msgs);
|
|
92
|
+
}
|
|
93
|
+
messageUnsubs = messageHandles.map(h => {
|
|
94
|
+
h.on('change', updateMessages);
|
|
95
|
+
return () => h.off('change', updateMessages);
|
|
96
|
+
});
|
|
97
|
+
updateMessages();
|
|
98
|
+
}
|
|
99
|
+
tHandle.on('change', updateMessagesAndSubs);
|
|
100
|
+
threadUnsub = () => tHandle.off('change', updateMessagesAndSubs);
|
|
101
|
+
await updateMessagesAndSubs();
|
|
102
|
+
}
|
|
103
|
+
subscribe();
|
|
104
|
+
return () => {
|
|
105
|
+
cancelled = true;
|
|
106
|
+
messageUnsubs.forEach(unsub => unsub());
|
|
107
|
+
if (threadUnsub)
|
|
108
|
+
threadUnsub();
|
|
109
|
+
};
|
|
110
|
+
}, [activeThread, repo]);
|
|
111
|
+
// If pendingChatInput changes, send it as a message
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (pendingChatInput && pendingChatInput.trim() !== '') {
|
|
114
|
+
handleChatInput(pendingChatInput);
|
|
115
|
+
onChatInputHandled();
|
|
116
|
+
}
|
|
117
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
118
|
+
}, [pendingChatInput]);
|
|
119
|
+
// Handle chat/command input
|
|
120
|
+
const handleChatInput = async (value) => {
|
|
121
|
+
// Navigation commands are handled by App
|
|
122
|
+
if (value.trim().startsWith('/')) {
|
|
123
|
+
handleInputSubmit(value);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!activeThread || !threadService || !workspace || !repo)
|
|
127
|
+
return;
|
|
128
|
+
const wsHandle = await repo.find(workspace.id);
|
|
129
|
+
const wsDoc = await wsHandle.doc();
|
|
130
|
+
// Support @agent mention override (e.g., "@dev: fix this bug")
|
|
131
|
+
const extractAgentMention = (text) => {
|
|
132
|
+
const leading = text.replace(/^\s+/, '');
|
|
133
|
+
const match = leading.match(/^@([a-zA-Z][\w-]*)\b:?\s*/);
|
|
134
|
+
if (!match)
|
|
135
|
+
return null;
|
|
136
|
+
const name = match[1].toLowerCase();
|
|
137
|
+
const message = leading.slice(match[0].length);
|
|
138
|
+
return { name, message };
|
|
139
|
+
};
|
|
140
|
+
async function loadAgentByName(name) {
|
|
141
|
+
const n = String(name || '').toLowerCase();
|
|
142
|
+
try {
|
|
143
|
+
if (n === 'dev' || n === 'developer') {
|
|
144
|
+
const url = new URL('../../../mod-agents/dev-agent/dist/index.mjs', import.meta.url).toString();
|
|
145
|
+
const mod = await import(url);
|
|
146
|
+
try {
|
|
147
|
+
log('[ThreadContainer] loaded dev agent module keys:', Object.keys(mod));
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
return mod.devAgent || mod.default?.devAgent || mod.default || mod;
|
|
151
|
+
}
|
|
152
|
+
if (n === 'planner' || n === 'plan') {
|
|
153
|
+
const url = new URL('../../../mod-agents/planner/dist/index.mjs', import.meta.url).toString();
|
|
154
|
+
const mod = await import(url);
|
|
155
|
+
try {
|
|
156
|
+
log('[ThreadContainer] loaded planner agent module keys:', Object.keys(mod));
|
|
157
|
+
}
|
|
158
|
+
catch { }
|
|
159
|
+
return mod.plannerAgent || mod.default?.plannerAgent || mod.default || mod;
|
|
160
|
+
}
|
|
161
|
+
if (n === 'general' || n === 'assistant') {
|
|
162
|
+
const url = new URL('../../../mod-agents/dev-agent/dist/index.mjs', import.meta.url).toString();
|
|
163
|
+
const mod = await import(url);
|
|
164
|
+
try {
|
|
165
|
+
log('[ThreadContainer] loaded general agent module keys:', Object.keys(mod));
|
|
166
|
+
}
|
|
167
|
+
catch { }
|
|
168
|
+
return mod.devAgent || mod.default?.devAgent || mod.default || mod;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
try {
|
|
173
|
+
log('[ThreadContainer] loadAgentByName error:', String(e));
|
|
174
|
+
}
|
|
175
|
+
catch { }
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
// Prefer explicit @agent mention without loading a default agent first to avoid unnecessary module graphs
|
|
180
|
+
let textToSend = value;
|
|
181
|
+
const mention = extractAgentMention(value);
|
|
182
|
+
let agent;
|
|
183
|
+
if (mention) {
|
|
184
|
+
agent = (await loadAgentByName(mention.name)) || { id: mention.name, name: mention.name };
|
|
185
|
+
textToSend = mention.message.trimStart();
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const baseAgentId = (wsDoc.activeAgents && wsDoc.activeAgents[0]) || 'general';
|
|
189
|
+
agent = (await loadAgentByName(baseAgentId)) || { id: baseAgentId, name: baseAgentId };
|
|
190
|
+
}
|
|
191
|
+
// 3. Stream agent chat (user message + agent response)
|
|
192
|
+
let sawChunk = false;
|
|
193
|
+
setAgentChunks([]); // Clear previous stream
|
|
194
|
+
try {
|
|
195
|
+
for await (const chunk of threadService.streamAgentChat({
|
|
196
|
+
threadId: activeThread.id,
|
|
197
|
+
userMessage: textToSend,
|
|
198
|
+
user: { id: 'user-1', name: 'You' },
|
|
199
|
+
workspace: wsDoc,
|
|
200
|
+
agent,
|
|
201
|
+
})) {
|
|
202
|
+
sawChunk = true;
|
|
203
|
+
setAgentChunks(prev => [...prev, chunk]); // Add chunk to static stream
|
|
204
|
+
}
|
|
205
|
+
if (!sawChunk) {
|
|
206
|
+
// Optionally show a message or log
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
// Optionally show error
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
return (_jsx(_Fragment, { children: _jsx(ThreadView, { activeThread: activeThread, messages: Array.isArray(messages) ? messages : [], parseContentSegments: parseContentSegments, messageKeyProp: "_contentHash" }) }));
|
|
214
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import SelectInput from 'ink-select-input';
|
|
5
|
+
import { useThreadsStore } from '../stores/use-threads-store.js';
|
|
6
|
+
export default function ThreadsListContainer({ repo, workspace, onSelect }) {
|
|
7
|
+
const threads = useThreadsStore(s => s.threads);
|
|
8
|
+
const loading = useThreadsStore(s => s.loading);
|
|
9
|
+
const error = useThreadsStore(s => s.error);
|
|
10
|
+
const fetchThreads = useThreadsStore(s => s.fetchThreads);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (repo && workspace)
|
|
13
|
+
fetchThreads(repo, workspace);
|
|
14
|
+
}, [repo, workspace, fetchThreads]);
|
|
15
|
+
if (loading)
|
|
16
|
+
return _jsx(Text, { children: "Loading threads..." });
|
|
17
|
+
if (error)
|
|
18
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
19
|
+
if (!threads.length)
|
|
20
|
+
return _jsx(Text, { children: "No threads found in workspace." });
|
|
21
|
+
const items = threads.map(t => ({ label: t.name, value: t.id }));
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select a thread:" }), _jsx(SelectInput, { items: items, onSelect: item => {
|
|
23
|
+
const thread = threads.find(t => t.id === item.value);
|
|
24
|
+
if (thread)
|
|
25
|
+
onSelect(thread);
|
|
26
|
+
} })] }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import SelectInput from 'ink-select-input';
|
|
5
|
+
import { useWorkspacesStore } from '../stores/use-workspaces-store.js';
|
|
6
|
+
export default function WorkspacesListContainer({ repo, onSelect }) {
|
|
7
|
+
const workspaces = useWorkspacesStore(s => s.workspaces);
|
|
8
|
+
const loading = useWorkspacesStore(s => s.loading);
|
|
9
|
+
const error = useWorkspacesStore(s => s.error);
|
|
10
|
+
const fetchWorkspaces = useWorkspacesStore(s => s.fetchWorkspaces);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (repo)
|
|
13
|
+
fetchWorkspaces(repo);
|
|
14
|
+
}, [repo, fetchWorkspaces]);
|
|
15
|
+
if (loading)
|
|
16
|
+
return _jsx(Text, { children: "Loading workspaces..." });
|
|
17
|
+
if (error)
|
|
18
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
19
|
+
if (!workspaces.length)
|
|
20
|
+
return _jsx(Text, { children: "No workspaces found in root doc." });
|
|
21
|
+
const items = workspaces.map(w => ({ label: w.name, value: w.id }));
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select a workspace:" }), _jsx(SelectInput, { items: items, onSelect: item => {
|
|
23
|
+
const ws = workspaces.find(w => w.id === item.value);
|
|
24
|
+
if (ws)
|
|
25
|
+
onSelect(ws);
|
|
26
|
+
} })] }));
|
|
27
|
+
}
|