vibe-coding-master 0.0.8 → 0.0.10
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 +35 -24
- package/dist/backend/adapters/filesystem.js +0 -7
- package/dist/backend/api/app-settings-routes.js +8 -0
- package/dist/backend/api/message-routes.js +3 -1
- package/dist/backend/api/session-routes.js +7 -1
- package/dist/backend/api/task-routes.js +3 -10
- package/dist/backend/api/translation-routes.js +21 -3
- package/dist/backend/runtime/terminal-submit.js +20 -0
- package/dist/backend/server.js +8 -4
- package/dist/backend/services/app-settings-service.js +118 -15
- package/dist/backend/services/claude-transcript-service.js +12 -8
- package/dist/backend/services/command-dispatcher.js +2 -1
- package/dist/backend/services/message-service.js +10 -6
- package/dist/backend/services/project-service.js +5 -27
- package/dist/backend/services/session-service.js +7 -4
- package/dist/backend/services/task-service.js +66 -57
- package/dist/backend/services/translation-service.js +264 -77
- package/dist/backend/templates/harness/gitignore.js +1 -4
- package/dist/shared/types/app-settings.js +1 -0
- package/dist-frontend/assets/index-B1vIIwLq.js +88 -0
- package/dist-frontend/assets/index-DPyKuEOz.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/cc-best-practices.md +4 -4
- package/docs/product-design.md +71 -31
- package/docs/v1-architecture-design.md +90 -56
- package/docs/v1-implementation-plan.md +76 -62
- package/package.json +3 -1
- package/dist/backend/ws/translation-ws.js +0 -35
- package/dist-frontend/assets/index-CuiNNOzj.css +0 -32
- package/dist-frontend/assets/index-D59GuHCR.js +0 -58
package/README.md
CHANGED
|
@@ -160,13 +160,13 @@ The workflow status is shown in the sidebar `Workflow` section. It is a soft gui
|
|
|
160
160
|
|
|
161
161
|
## Task Worktree Management
|
|
162
162
|
|
|
163
|
-
VCM uses task-level worktree management:
|
|
163
|
+
VCM uses task-level worktree management by default:
|
|
164
164
|
|
|
165
165
|
```text
|
|
166
166
|
one task = one branch + one git worktree + one handoff directory + one role-session set
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
The default when creating a task:
|
|
169
|
+
The `Create worktree and branch` option is selected by default when creating a task:
|
|
170
170
|
|
|
171
171
|
- task name: `<task>`
|
|
172
172
|
- branch: `feature/<task>`
|
|
@@ -175,11 +175,13 @@ The default when creating a task:
|
|
|
175
175
|
|
|
176
176
|
VCM will not create worktrees per role. `project-manager`, `architect`, `coder`, and `reviewer` for the same task share the same task worktree.
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
The user can turn this option off. In that mode, VCM creates the task metadata and handoff structure in the connected repository, records the current branch, and starts role sessions from the connected repository path.
|
|
179
|
+
|
|
180
|
+
VCM will not offer a separate `Create task worktree` button after a task exists, and a task should not be switched to another branch/worktree mode after creation.
|
|
179
181
|
|
|
180
182
|
Because worktrees live under `.ai/vcm/worktrees/`, the connected repository must ignore `.ai/vcm/`. Apply the VCM Harness before creating tasks so `.gitignore` contains the managed ignore block. The base repository must also be clean because the task branch/worktree is created from the connected repo's current `HEAD`.
|
|
181
183
|
|
|
182
|
-
When a task is complete, VCM provides a
|
|
184
|
+
When a task is complete, VCM provides a red `Close Task` action. Closing a worktree-backed task shows a destructive confirmation, then deletes the task worktree, deletes the task branch by default, removes the base task index entry, and removes task runtime metadata. VCM does not check running sessions or uncommitted changes before closing. Tasks created without a worktree only remove VCM metadata because they do not own a separate branch/worktree.
|
|
183
185
|
|
|
184
186
|
## Sidebar UI
|
|
185
187
|
|
|
@@ -188,7 +190,7 @@ The left sidebar is intentionally compact and collapsible:
|
|
|
188
190
|
- `Repository Path`: path input on one row; `Recent` and `Connect` on the next row.
|
|
189
191
|
- `Repository`: connected path, branch, and working tree state. `Working tree: uncommitted changes` means `git status --porcelain` is not empty.
|
|
190
192
|
- `Workflow`: current soft gate and five workflow steps.
|
|
191
|
-
- `Settings`: `Messages`, `Events`, and the `Auto orchestration` on/off toggle.
|
|
193
|
+
- `Settings`: `Theme`, `Messages`, `Events`, and the `Auto orchestration` on/off toggle.
|
|
192
194
|
- `VCM Harness`: status for `CLAUDE.md`, role agent files, and `.gitignore`.
|
|
193
195
|
- `New Task`: one `task name` input.
|
|
194
196
|
- `Tasks`: task list and task status.
|
|
@@ -207,17 +209,24 @@ Translation settings are local and stored in:
|
|
|
207
209
|
|
|
208
210
|
The same file stores recent repository paths. The translation API key is stored locally under `translation.secrets.apiKey`; it is not written to the connected repository, `.ai/handoffs`, raw terminal logs, or git diffs.
|
|
209
211
|
|
|
212
|
+
The sidebar `Settings` section also stores the UI theme preference in this file. The default is `system`, which follows the OS/browser color-scheme preference; users can cycle between `System`, `Light`, and `Dark`.
|
|
213
|
+
|
|
210
214
|
Translation behavior:
|
|
211
215
|
|
|
212
216
|
- Provider type is OpenAI-compatible chat completions.
|
|
213
217
|
- Prompt slots are `zh-to-en`, `zh-to-en-with-context`, and `en-to-zh`.
|
|
214
|
-
- The settings modal shows
|
|
218
|
+
- The settings modal shows all three prompt slots as direct editors and includes `Reset prompts` to restore the built-in defaults.
|
|
215
219
|
- Claude Code output translation reads semantic Claude transcript JSONL files under `~/.claude/projects`, not raw PTY output.
|
|
220
|
+
- VCM tails those transcript files in the backend. Closing the translation panel does not stop capture; the tailer stops only when the role session is stopped/restarted or the task is closed.
|
|
221
|
+
- Translation events are cached under the task runtime repo at `.ai/vcm/translation/<task>/<role>/<session-id>.jsonl` and delivered to the frontend through HTTP polling.
|
|
222
|
+
- The polling cursor is the next expected seq: `after=18` acknowledges seq `1..17` and returns seq `18+`; there is no snapshot mismatch error.
|
|
216
223
|
- Assistant prose is shown as English source while translating, then replaced by the translated Chinese result.
|
|
224
|
+
- Assistant prose renders Markdown in the panel, including headings, lists, code fences, tables, and links.
|
|
217
225
|
- Tool calls and tool results are preserved as dim one-line rows such as `● Bash({"command":"npm test"})`.
|
|
218
226
|
- User input uses one textarea. Press `Enter` to translate or send the current English draft; press `Shift+Enter` for a newline.
|
|
219
227
|
- After user input is translated, the English draft replaces the original text in the same textarea.
|
|
220
228
|
- `Send English` writes the current English draft to the active embedded terminal and submits it.
|
|
229
|
+
- Automatic terminal submission uses bracketed paste first, then sends Enter separately for Claude Code TUI reliability.
|
|
221
230
|
- The translation panel `Auto-send` toggle sends the translated draft automatically when translation succeeds without warnings.
|
|
222
231
|
|
|
223
232
|
## Project Harness
|
|
@@ -246,11 +255,10 @@ For `.gitignore`, VCM uses a gitignore-native managed block:
|
|
|
246
255
|
```gitignore
|
|
247
256
|
# VCM:BEGIN version=1
|
|
248
257
|
.ai/vcm/
|
|
249
|
-
.vcm/
|
|
250
258
|
# VCM:END
|
|
251
259
|
```
|
|
252
260
|
|
|
253
|
-
`.ai/vcm/` is the active VCM local control area.
|
|
261
|
+
`.ai/vcm/` is the active VCM local control area. The base repo keeps the task index and nested worktrees; each task runtime repo keeps its own session, message, orchestration, and translation state.
|
|
254
262
|
|
|
255
263
|
After applying harness changes, VCM reports the exact files changed and reminds the user to review and commit them before starting long-running work.
|
|
256
264
|
|
|
@@ -282,8 +290,8 @@ vcmctl inbox
|
|
|
282
290
|
Durable message and handoff files:
|
|
283
291
|
|
|
284
292
|
```text
|
|
285
|
-
.ai/vcm/messages/<task>.jsonl
|
|
286
|
-
.ai/vcm/orchestration/<task>.json
|
|
293
|
+
.ai/vcm/messages/<task>.jsonl # under the task runtime repo
|
|
294
|
+
.ai/vcm/orchestration/<task>.json # under the task runtime repo
|
|
287
295
|
.ai/handoffs/<task>/messages/<message-id>.md
|
|
288
296
|
.ai/handoffs/<task>/role-commands/
|
|
289
297
|
.ai/handoffs/<task>/logs/
|
|
@@ -317,7 +325,7 @@ The backend state model still contains a `paused` field for compatibility with e
|
|
|
317
325
|
Each role session stores its Claude session id and transcript path under:
|
|
318
326
|
|
|
319
327
|
```text
|
|
320
|
-
.ai/vcm/sessions/<task>.json
|
|
328
|
+
.ai/vcm/sessions/<task>.json # under the task runtime repo
|
|
321
329
|
```
|
|
322
330
|
|
|
323
331
|
Session buttons behave as follows:
|
|
@@ -332,21 +340,24 @@ Session buttons behave as follows:
|
|
|
332
340
|
For a connected repository, VCM uses:
|
|
333
341
|
|
|
334
342
|
```text
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
343
|
+
~/.vcm/projects/<project-id>/config.json
|
|
344
|
+
<baseRepoRoot>/.ai/vcm/tasks/<task>.json
|
|
345
|
+
<baseRepoRoot>/.ai/vcm/worktrees/<task>/
|
|
346
|
+
<taskRepoRoot>/.ai/vcm/sessions/<task>.json
|
|
347
|
+
<taskRepoRoot>/.ai/vcm/messages/<task>.jsonl
|
|
348
|
+
<taskRepoRoot>/.ai/vcm/orchestration/<task>.json
|
|
349
|
+
<taskRepoRoot>/.ai/vcm/translation/<task>/
|
|
350
|
+
<taskRepoRoot>/.ai/handoffs/<task>/architecture-plan.md
|
|
351
|
+
<taskRepoRoot>/.ai/handoffs/<task>/implementation-log.md
|
|
352
|
+
<taskRepoRoot>/.ai/handoffs/<task>/validation-log.md
|
|
353
|
+
<taskRepoRoot>/.ai/handoffs/<task>/review-report.md
|
|
354
|
+
<taskRepoRoot>/.ai/handoffs/<task>/docs-sync-report.md
|
|
355
|
+
<taskRepoRoot>/.ai/handoffs/<task>/role-commands/{architect,coder,reviewer}.md
|
|
356
|
+
<taskRepoRoot>/.ai/handoffs/<task>/logs/{project-manager,architect,coder,reviewer}.log
|
|
348
357
|
```
|
|
349
358
|
|
|
359
|
+
The project config is stored under `~/.vcm` so it is durable local app state and is not hidden inside a Git-ignored repository directory. For worktree-backed tasks, `taskRepoRoot` is `<baseRepoRoot>/.ai/vcm/worktrees/<task>`; for inline tasks, `taskRepoRoot` is the connected base repo.
|
|
360
|
+
|
|
350
361
|
## Packaging
|
|
351
362
|
|
|
352
363
|
The npm package publishes built output, not raw TypeScript entry files. `package.json` includes:
|
|
@@ -47,13 +47,6 @@ export function createNodeFileSystemAdapter() {
|
|
|
47
47
|
await this.writeText(targetPath, content);
|
|
48
48
|
return true;
|
|
49
49
|
},
|
|
50
|
-
async copyDir(sourcePath, targetPath) {
|
|
51
|
-
await fs.cp(sourcePath, targetPath, {
|
|
52
|
-
recursive: true,
|
|
53
|
-
force: false,
|
|
54
|
-
errorOnExist: false
|
|
55
|
-
});
|
|
56
|
-
},
|
|
57
50
|
async removePath(targetPath, options = {}) {
|
|
58
51
|
await fs.rm(targetPath, {
|
|
59
52
|
recursive: options.recursive ?? false,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function registerAppSettingsRoutes(app, deps) {
|
|
2
|
+
app.get("/api/settings/preferences", async () => {
|
|
3
|
+
return deps.appSettings.getPreferences();
|
|
4
|
+
});
|
|
5
|
+
app.put("/api/settings/preferences", async (request) => {
|
|
6
|
+
return deps.appSettings.updatePreferences(request.body ?? {});
|
|
7
|
+
});
|
|
8
|
+
}
|
|
@@ -71,9 +71,11 @@ async function getRouteContext(deps, taskSlug) {
|
|
|
71
71
|
const project = await requireCurrentProject(deps.projectService);
|
|
72
72
|
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
73
73
|
const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
|
|
74
|
+
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
74
75
|
return {
|
|
75
76
|
repoRoot: project.repoRoot,
|
|
76
|
-
taskRepoRoot
|
|
77
|
+
taskRepoRoot,
|
|
78
|
+
stateRepoRoot: taskRepoRoot,
|
|
77
79
|
stateRoot: config.stateRoot,
|
|
78
80
|
handoffDir: task.handoffDir,
|
|
79
81
|
taskSlug
|
|
@@ -13,11 +13,17 @@ export function registerSessionRoutes(app, deps) {
|
|
|
13
13
|
app.post("/api/tasks/:taskSlug/sessions/:role/stop", async (request) => {
|
|
14
14
|
const project = await requireCurrentProject(deps.projectService);
|
|
15
15
|
const role = parseRole(request.params.role);
|
|
16
|
-
|
|
16
|
+
const session = await deps.sessionService.stopRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
17
|
+
await deps.translationService.stopSession(session.id);
|
|
18
|
+
return session;
|
|
17
19
|
});
|
|
18
20
|
app.post("/api/tasks/:taskSlug/sessions/:role/restart", async (request) => {
|
|
19
21
|
const project = await requireCurrentProject(deps.projectService);
|
|
20
22
|
const role = parseRole(request.params.role);
|
|
23
|
+
const existing = await deps.sessionService.getRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
24
|
+
if (existing) {
|
|
25
|
+
await deps.translationService.stopSession(existing.id, { clearCache: true });
|
|
26
|
+
}
|
|
21
27
|
return deps.sessionService.restartRoleSession(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
22
28
|
});
|
|
23
29
|
app.post("/api/tasks/:taskSlug/sessions/:role/resume", async (request) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { VcmError } from "../errors.js";
|
|
2
|
+
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
2
3
|
export function registerTaskRoutes(app, deps) {
|
|
3
4
|
app.get("/api/tasks", async () => {
|
|
4
5
|
const project = await requireCurrentProject(deps.projectService);
|
|
@@ -18,16 +19,8 @@ export function registerTaskRoutes(app, deps) {
|
|
|
18
19
|
});
|
|
19
20
|
app.post("/api/tasks/:taskSlug/cleanup", async (request) => {
|
|
20
21
|
const project = await requireCurrentProject(deps.projectService);
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
if (running.length > 0) {
|
|
24
|
-
throw new VcmError({
|
|
25
|
-
code: "TASK_SESSIONS_RUNNING",
|
|
26
|
-
message: "Stop all role sessions before cleaning up the task.",
|
|
27
|
-
statusCode: 409,
|
|
28
|
-
hint: running.map((session) => session.role).join(", ")
|
|
29
|
-
});
|
|
30
|
-
}
|
|
22
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
23
|
+
await deps.translationService.stopTask(getTaskRuntimeRepoRoot(task), request.params.taskSlug, { clearCache: true });
|
|
31
24
|
return deps.taskService.cleanupTask(project.repoRoot, request.params.taskSlug, request.body ?? {});
|
|
32
25
|
});
|
|
33
26
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
|
+
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
3
4
|
export function registerTranslationRoutes(app, deps) {
|
|
4
5
|
app.get("/api/translation/settings", async () => {
|
|
5
6
|
return deps.translationService.getSettings();
|
|
@@ -14,12 +15,28 @@ export function registerTranslationRoutes(app, deps) {
|
|
|
14
15
|
app.post("/api/translation/test", async () => {
|
|
15
16
|
return deps.translationService.testProvider();
|
|
16
17
|
});
|
|
18
|
+
app.post("/api/tasks/:taskSlug/sessions/:role/translation/start", async (request) => {
|
|
19
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
20
|
+
const role = parseRole(request.params.role);
|
|
21
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
22
|
+
return deps.translationService.startSession({
|
|
23
|
+
repoRoot: project.repoRoot,
|
|
24
|
+
taskRepoRoot: getTaskRuntimeRepoRoot(task),
|
|
25
|
+
taskSlug: request.params.taskSlug,
|
|
26
|
+
role
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
app.get("/api/translation/sessions/:sessionId/events", async (request) => {
|
|
30
|
+
await requireCurrentProject(deps.projectService);
|
|
31
|
+
return deps.translationService.pollSessionEvents(request.params.sessionId, Number(request.query.after ?? "1"), request.query.limit === undefined ? undefined : Number(request.query.limit));
|
|
32
|
+
});
|
|
17
33
|
app.post("/api/tasks/:taskSlug/sessions/:role/translation/input", async (request) => {
|
|
18
34
|
const project = await requireCurrentProject(deps.projectService);
|
|
19
35
|
const role = parseRole(request.params.role);
|
|
20
|
-
await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
36
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
21
37
|
return deps.translationService.translateUserInput({
|
|
22
38
|
repoRoot: project.repoRoot,
|
|
39
|
+
taskRepoRoot: getTaskRuntimeRepoRoot(task),
|
|
23
40
|
taskSlug: request.params.taskSlug,
|
|
24
41
|
role,
|
|
25
42
|
...(request.body ?? { text: "" })
|
|
@@ -28,9 +45,10 @@ export function registerTranslationRoutes(app, deps) {
|
|
|
28
45
|
app.post("/api/tasks/:taskSlug/sessions/:role/translation/send", async (request) => {
|
|
29
46
|
const project = await requireCurrentProject(deps.projectService);
|
|
30
47
|
const role = parseRole(request.params.role);
|
|
31
|
-
await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
48
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
32
49
|
await deps.translationService.sendTranslatedInput({
|
|
33
50
|
repoRoot: project.repoRoot,
|
|
51
|
+
taskRepoRoot: getTaskRuntimeRepoRoot(task),
|
|
34
52
|
taskSlug: request.params.taskSlug,
|
|
35
53
|
role,
|
|
36
54
|
englishText: request.body?.englishText ?? ""
|
|
@@ -39,7 +57,7 @@ export function registerTranslationRoutes(app, deps) {
|
|
|
39
57
|
});
|
|
40
58
|
app.post("/api/translation/sessions/:sessionId/clear", async (request) => {
|
|
41
59
|
await requireCurrentProject(deps.projectService);
|
|
42
|
-
deps.translationService.clearSession(request.params.sessionId);
|
|
60
|
+
await deps.translationService.clearSession(request.params.sessionId);
|
|
43
61
|
return { ok: true };
|
|
44
62
|
});
|
|
45
63
|
app.post("/api/translation/sessions/:sessionId/retry/:translationId", async (request) => {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
2
|
+
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
3
|
+
const DEFAULT_ENTER_DELAY_MS = 75;
|
|
4
|
+
export async function submitTerminalInput(runtime, sessionId, text, options = {}) {
|
|
5
|
+
runtime.write(sessionId, formatTerminalPaste(text));
|
|
6
|
+
await delay(options.enterDelayMs ?? DEFAULT_ENTER_DELAY_MS);
|
|
7
|
+
runtime.write(sessionId, "\r");
|
|
8
|
+
}
|
|
9
|
+
export function formatTerminalPaste(text) {
|
|
10
|
+
return `${BRACKETED_PASTE_START}${normalizeTerminalSubmitText(text)}${BRACKETED_PASTE_END}`;
|
|
11
|
+
}
|
|
12
|
+
export function normalizeTerminalSubmitText(text) {
|
|
13
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n+$/g, "");
|
|
14
|
+
}
|
|
15
|
+
function delay(ms) {
|
|
16
|
+
if (ms <= 0) {
|
|
17
|
+
return Promise.resolve();
|
|
18
|
+
}
|
|
19
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
}
|
package/dist/backend/server.js
CHANGED
|
@@ -21,6 +21,7 @@ import { createMessageService } from "./services/message-service.js";
|
|
|
21
21
|
import { createStatusService } from "./services/status-service.js";
|
|
22
22
|
import { createTaskService } from "./services/task-service.js";
|
|
23
23
|
import { createTranslationService } from "./services/translation-service.js";
|
|
24
|
+
import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
|
|
24
25
|
import { registerArtifactRoutes } from "./api/artifact-routes.js";
|
|
25
26
|
import { registerHarnessRoutes } from "./api/harness-routes.js";
|
|
26
27
|
import { registerMessageRoutes } from "./api/message-routes.js";
|
|
@@ -29,7 +30,6 @@ import { registerSessionRoutes } from "./api/session-routes.js";
|
|
|
29
30
|
import { registerTaskRoutes } from "./api/task-routes.js";
|
|
30
31
|
import { registerTranslationRoutes } from "./api/translation-routes.js";
|
|
31
32
|
import { registerTerminalWs } from "./ws/terminal-ws.js";
|
|
32
|
-
import { registerTranslationWs } from "./ws/translation-ws.js";
|
|
33
33
|
import { toVcmError } from "./errors.js";
|
|
34
34
|
export async function createServer(deps, options = {}) {
|
|
35
35
|
const app = Fastify({
|
|
@@ -45,6 +45,7 @@ export async function createServer(deps, options = {}) {
|
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
|
+
registerAppSettingsRoutes(app, { appSettings: deps.appSettings });
|
|
48
49
|
registerProjectRoutes(app, { projectService: deps.projectService });
|
|
49
50
|
registerHarnessRoutes(app, {
|
|
50
51
|
projectService: deps.projectService,
|
|
@@ -54,12 +55,13 @@ export async function createServer(deps, options = {}) {
|
|
|
54
55
|
projectService: deps.projectService,
|
|
55
56
|
taskService: deps.taskService,
|
|
56
57
|
statusService: deps.statusService,
|
|
57
|
-
|
|
58
|
+
translationService: deps.translationService
|
|
58
59
|
});
|
|
59
60
|
registerSessionRoutes(app, {
|
|
60
61
|
projectService: deps.projectService,
|
|
61
62
|
sessionService: deps.sessionService,
|
|
62
|
-
commandDispatcher: deps.commandDispatcher
|
|
63
|
+
commandDispatcher: deps.commandDispatcher,
|
|
64
|
+
translationService: deps.translationService
|
|
63
65
|
});
|
|
64
66
|
registerArtifactRoutes(app, {
|
|
65
67
|
projectService: deps.projectService,
|
|
@@ -77,7 +79,6 @@ export async function createServer(deps, options = {}) {
|
|
|
77
79
|
translationService: deps.translationService
|
|
78
80
|
});
|
|
79
81
|
registerTerminalWs(app, { runtime: deps.runtime });
|
|
80
|
-
registerTranslationWs(app, { translationService: deps.translationService });
|
|
81
82
|
if (options.staticDir) {
|
|
82
83
|
await app.register(fastifyStatic, {
|
|
83
84
|
root: options.staticDir,
|
|
@@ -149,10 +150,13 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
149
150
|
sessionRegistry: registry,
|
|
150
151
|
transcripts: createClaudeTranscriptService(),
|
|
151
152
|
sessionService,
|
|
153
|
+
fs,
|
|
154
|
+
projectService,
|
|
152
155
|
appSettings,
|
|
153
156
|
provider: createOpenAiCompatibleTranslationProvider()
|
|
154
157
|
});
|
|
155
158
|
return {
|
|
159
|
+
appSettings,
|
|
156
160
|
projectService,
|
|
157
161
|
taskService,
|
|
158
162
|
sessionService,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
const MAX_RECENT_REPOSITORIES = 5;
|
|
4
5
|
export function createAppSettingsService(deps) {
|
|
5
6
|
const settingsPath = deps.settingsPath ?? path.join(homedir(), ".vcm", "settings.json");
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const legacyTranslationPath = deps.legacyTranslationPath
|
|
9
|
-
?? path.join(homedir(), ".vibe-coding-master", "translation.json");
|
|
7
|
+
const settingsRoot = path.dirname(settingsPath);
|
|
8
|
+
const projectIndexPath = path.join(settingsRoot, "projects", "index.json");
|
|
10
9
|
let cachedSettings = null;
|
|
10
|
+
let cachedProjectIndex = null;
|
|
11
11
|
async function loadSettings() {
|
|
12
12
|
if (cachedSettings) {
|
|
13
13
|
return cachedSettings;
|
|
@@ -17,20 +17,9 @@ export function createAppSettingsService(deps) {
|
|
|
17
17
|
if (await deps.fs.pathExists(settingsPath)) {
|
|
18
18
|
raw = await deps.fs.readJson(settingsPath);
|
|
19
19
|
}
|
|
20
|
-
else if (await deps.fs.pathExists(legacySettingsPath)) {
|
|
21
|
-
raw = await deps.fs.readJson(legacySettingsPath);
|
|
22
|
-
shouldSave = true;
|
|
23
|
-
}
|
|
24
20
|
else {
|
|
25
21
|
shouldSave = true;
|
|
26
22
|
}
|
|
27
|
-
if (!raw.translation && await deps.fs.pathExists(legacyTranslationPath)) {
|
|
28
|
-
raw = {
|
|
29
|
-
...raw,
|
|
30
|
-
translation: normalizeTranslationConfig(await deps.fs.readJson(legacyTranslationPath))
|
|
31
|
-
};
|
|
32
|
-
shouldSave = true;
|
|
33
|
-
}
|
|
34
23
|
cachedSettings = normalizeSettingsFile(raw);
|
|
35
24
|
if (shouldSave) {
|
|
36
25
|
await saveSettings(cachedSettings);
|
|
@@ -41,8 +30,43 @@ export function createAppSettingsService(deps) {
|
|
|
41
30
|
cachedSettings = settings;
|
|
42
31
|
await deps.fs.writeJsonAtomic(settingsPath, settings);
|
|
43
32
|
}
|
|
33
|
+
async function loadProjectIndex() {
|
|
34
|
+
if (cachedProjectIndex) {
|
|
35
|
+
return cachedProjectIndex;
|
|
36
|
+
}
|
|
37
|
+
if (await deps.fs.pathExists(projectIndexPath)) {
|
|
38
|
+
cachedProjectIndex = normalizeProjectIndexFile(await deps.fs.readJson(projectIndexPath));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
cachedProjectIndex = { version: 1, projects: [] };
|
|
42
|
+
await saveProjectIndex(cachedProjectIndex);
|
|
43
|
+
}
|
|
44
|
+
return cachedProjectIndex;
|
|
45
|
+
}
|
|
46
|
+
async function saveProjectIndex(index) {
|
|
47
|
+
cachedProjectIndex = normalizeProjectIndexFile(index);
|
|
48
|
+
await deps.fs.writeJsonAtomic(projectIndexPath, cachedProjectIndex);
|
|
49
|
+
}
|
|
50
|
+
function getProjectConfigPath(repoRoot) {
|
|
51
|
+
return path.join(settingsRoot, "projects", getProjectId(repoRoot), "config.json");
|
|
52
|
+
}
|
|
44
53
|
return {
|
|
45
54
|
loadSettings,
|
|
55
|
+
async getPreferences() {
|
|
56
|
+
return (await loadSettings()).preferences;
|
|
57
|
+
},
|
|
58
|
+
async updatePreferences(input) {
|
|
59
|
+
const current = await loadSettings();
|
|
60
|
+
const preferences = normalizePreferences({
|
|
61
|
+
...current.preferences,
|
|
62
|
+
...input
|
|
63
|
+
});
|
|
64
|
+
await saveSettings({
|
|
65
|
+
...current,
|
|
66
|
+
preferences
|
|
67
|
+
});
|
|
68
|
+
return preferences;
|
|
69
|
+
},
|
|
46
70
|
async updateTranslationConfig(config) {
|
|
47
71
|
const current = await loadSettings();
|
|
48
72
|
const translation = normalizeTranslationConfig(config) ?? { settings: {}, secrets: {} };
|
|
@@ -74,18 +98,97 @@ export function createAppSettingsService(deps) {
|
|
|
74
98
|
});
|
|
75
99
|
return recentRepositoryPaths;
|
|
76
100
|
},
|
|
101
|
+
loadProjectIndex,
|
|
102
|
+
async loadProjectConfig(repoRoot) {
|
|
103
|
+
const configPath = getProjectConfigPath(repoRoot);
|
|
104
|
+
if (!(await deps.fs.pathExists(configPath))) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return deps.fs.readJson(configPath);
|
|
108
|
+
},
|
|
109
|
+
async saveProjectConfig(config) {
|
|
110
|
+
const configPath = getProjectConfigPath(config.repoRoot);
|
|
111
|
+
await deps.fs.writeJsonAtomic(configPath, config);
|
|
112
|
+
const projectId = getProjectId(config.repoRoot);
|
|
113
|
+
const current = await loadProjectIndex();
|
|
114
|
+
const projects = [
|
|
115
|
+
{
|
|
116
|
+
projectId,
|
|
117
|
+
repoRoot: config.repoRoot,
|
|
118
|
+
configPath,
|
|
119
|
+
lastOpenedAt: new Date().toISOString()
|
|
120
|
+
},
|
|
121
|
+
...current.projects.filter((entry) => entry.projectId !== projectId)
|
|
122
|
+
];
|
|
123
|
+
await saveProjectIndex({
|
|
124
|
+
version: 1,
|
|
125
|
+
projects
|
|
126
|
+
});
|
|
127
|
+
return config;
|
|
128
|
+
},
|
|
77
129
|
getSettingsPath() {
|
|
78
130
|
return settingsPath;
|
|
131
|
+
},
|
|
132
|
+
getProjectIndexPath() {
|
|
133
|
+
return projectIndexPath;
|
|
134
|
+
},
|
|
135
|
+
getProjectConfigPath
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function getProjectId(repoRoot) {
|
|
139
|
+
return createHash("sha256")
|
|
140
|
+
.update(path.resolve(repoRoot))
|
|
141
|
+
.digest("hex")
|
|
142
|
+
.slice(0, 16);
|
|
143
|
+
}
|
|
144
|
+
function normalizeProjectIndexFile(input) {
|
|
145
|
+
const rawProjects = Array.isArray(input.projects) ? input.projects : [];
|
|
146
|
+
const projects = [];
|
|
147
|
+
const seen = new Set();
|
|
148
|
+
for (const value of rawProjects) {
|
|
149
|
+
if (!isObject(value)) {
|
|
150
|
+
continue;
|
|
79
151
|
}
|
|
152
|
+
const projectId = typeof value.projectId === "string" ? value.projectId.trim() : "";
|
|
153
|
+
const repoRoot = typeof value.repoRoot === "string" ? value.repoRoot.trim() : "";
|
|
154
|
+
const configPath = typeof value.configPath === "string" ? value.configPath.trim() : "";
|
|
155
|
+
const lastOpenedAt = typeof value.lastOpenedAt === "string" ? value.lastOpenedAt.trim() : "";
|
|
156
|
+
if (!projectId || !repoRoot || !configPath || seen.has(projectId)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
seen.add(projectId);
|
|
160
|
+
projects.push({
|
|
161
|
+
projectId,
|
|
162
|
+
repoRoot,
|
|
163
|
+
configPath,
|
|
164
|
+
lastOpenedAt: lastOpenedAt || new Date(0).toISOString()
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
version: 1,
|
|
169
|
+
projects
|
|
80
170
|
};
|
|
81
171
|
}
|
|
82
172
|
function normalizeSettingsFile(input) {
|
|
83
173
|
return {
|
|
84
174
|
version: 1,
|
|
175
|
+
preferences: normalizePreferences(input.preferences),
|
|
85
176
|
translation: normalizeTranslationConfig(input.translation),
|
|
86
177
|
recentRepositoryPaths: normalizeRecentRepositoryPaths(input.recentRepositoryPaths)
|
|
87
178
|
};
|
|
88
179
|
}
|
|
180
|
+
function normalizePreferences(input) {
|
|
181
|
+
const candidate = isObject(input) ? input : {};
|
|
182
|
+
return {
|
|
183
|
+
themeMode: normalizeThemeMode(candidate.themeMode)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function normalizeThemeMode(input) {
|
|
187
|
+
if (input === "light" || input === "dark" || input === "system") {
|
|
188
|
+
return input;
|
|
189
|
+
}
|
|
190
|
+
return "system";
|
|
191
|
+
}
|
|
89
192
|
function normalizeTranslationConfig(input) {
|
|
90
193
|
if (!input || typeof input !== "object") {
|
|
91
194
|
return undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync, watch as fsWatch } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
const DEFAULT_TAIL_POLL_INTERVAL_MS =
|
|
4
|
+
const DEFAULT_TAIL_POLL_INTERVAL_MS = 200;
|
|
5
5
|
/**
|
|
6
6
|
* Adapted from CodingForMoney/cc-pm's transcript tailer.
|
|
7
7
|
*
|
|
@@ -37,16 +37,16 @@ export class TranscriptTail {
|
|
|
37
37
|
this.buffer = "";
|
|
38
38
|
try {
|
|
39
39
|
this.watcher = fsWatch(this.path, () => {
|
|
40
|
-
this.scheduleFlush();
|
|
40
|
+
this.scheduleFlush("watch");
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
catch {
|
|
44
44
|
this.watcher = null;
|
|
45
45
|
}
|
|
46
46
|
this.pollTimer = setInterval(() => {
|
|
47
|
-
this.scheduleFlush();
|
|
47
|
+
this.scheduleFlush("poll");
|
|
48
48
|
}, opts?.pollIntervalMs ?? DEFAULT_TAIL_POLL_INTERVAL_MS);
|
|
49
|
-
this.scheduleFlush();
|
|
49
|
+
this.scheduleFlush("initial");
|
|
50
50
|
}
|
|
51
51
|
stop() {
|
|
52
52
|
if (this.watcher) {
|
|
@@ -58,23 +58,26 @@ export class TranscriptTail {
|
|
|
58
58
|
this.pollTimer = null;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
scheduleFlush() {
|
|
61
|
+
scheduleFlush(source) {
|
|
62
62
|
if (this.flushing || this.flushScheduled) {
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
this.flushScheduled = true;
|
|
66
66
|
setImmediate(() => {
|
|
67
67
|
this.flushScheduled = false;
|
|
68
|
-
this.flush();
|
|
68
|
+
this.flush(source);
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
-
flush() {
|
|
71
|
+
flush(source) {
|
|
72
72
|
if (this.flushing) {
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
this.flushing = true;
|
|
76
76
|
try {
|
|
77
77
|
const stat = statSync(this.path);
|
|
78
|
+
if (source === "poll") {
|
|
79
|
+
this.handlers.onPoll?.(new Date().toISOString());
|
|
80
|
+
}
|
|
78
81
|
if (stat.size < this.offset) {
|
|
79
82
|
this.offset = stat.size;
|
|
80
83
|
this.buffer = "";
|
|
@@ -173,7 +176,8 @@ export function createClaudeTranscriptService() {
|
|
|
173
176
|
try {
|
|
174
177
|
tail = new TranscriptTail(transcriptPath, {
|
|
175
178
|
onContent: listener,
|
|
176
|
-
onError: options.onError
|
|
179
|
+
onError: options.onError,
|
|
180
|
+
onPoll: options.onPoll
|
|
177
181
|
});
|
|
178
182
|
tail.start({
|
|
179
183
|
replayLastN: options.replayLastN,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { VcmError } from "../errors.js";
|
|
2
|
+
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
2
3
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
3
4
|
export function createCommandDispatcher(deps) {
|
|
4
5
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
@@ -26,7 +27,7 @@ export function createCommandDispatcher(deps) {
|
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
const instruction = `Please read and execute the role command at: ${commandPath}`;
|
|
29
|
-
deps.runtime
|
|
30
|
+
await submitTerminalInput(deps.runtime, session.id, instruction);
|
|
30
31
|
return {
|
|
31
32
|
taskSlug: input.taskSlug,
|
|
32
33
|
role: input.role,
|