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.
Files changed (30) hide show
  1. package/README.md +35 -24
  2. package/dist/backend/adapters/filesystem.js +0 -7
  3. package/dist/backend/api/app-settings-routes.js +8 -0
  4. package/dist/backend/api/message-routes.js +3 -1
  5. package/dist/backend/api/session-routes.js +7 -1
  6. package/dist/backend/api/task-routes.js +3 -10
  7. package/dist/backend/api/translation-routes.js +21 -3
  8. package/dist/backend/runtime/terminal-submit.js +20 -0
  9. package/dist/backend/server.js +8 -4
  10. package/dist/backend/services/app-settings-service.js +118 -15
  11. package/dist/backend/services/claude-transcript-service.js +12 -8
  12. package/dist/backend/services/command-dispatcher.js +2 -1
  13. package/dist/backend/services/message-service.js +10 -6
  14. package/dist/backend/services/project-service.js +5 -27
  15. package/dist/backend/services/session-service.js +7 -4
  16. package/dist/backend/services/task-service.js +66 -57
  17. package/dist/backend/services/translation-service.js +264 -77
  18. package/dist/backend/templates/harness/gitignore.js +1 -4
  19. package/dist/shared/types/app-settings.js +1 -0
  20. package/dist-frontend/assets/index-B1vIIwLq.js +88 -0
  21. package/dist-frontend/assets/index-DPyKuEOz.css +32 -0
  22. package/dist-frontend/index.html +2 -2
  23. package/docs/cc-best-practices.md +4 -4
  24. package/docs/product-design.md +71 -31
  25. package/docs/v1-architecture-design.md +90 -56
  26. package/docs/v1-implementation-plan.md +76 -62
  27. package/package.json +3 -1
  28. package/dist/backend/ws/translation-ws.js +0 -35
  29. package/dist-frontend/assets/index-CuiNNOzj.css +0 -32
  30. package/dist-frontend/assets/index-D59GuHCR.js +0 -58
@@ -1,11 +1,13 @@
1
+ import path from "node:path";
1
2
  import { TRANSLATION_PROMPT_KEYS } from "../../shared/types/translation.js";
2
3
  import { TranslationProviderError } from "../adapters/translation-provider.js";
3
4
  import { VcmError } from "../errors.js";
5
+ import { submitTerminalInput } from "../runtime/terminal-submit.js";
4
6
  import { buildTranslationPrompt, getTranslationPromptPreviews, parseTranslationWarning } from "./translation-prompts.js";
5
7
  import { createTranslationQueueRegistry } from "./translation-queue.js";
6
8
  const DEFAULT_SETTINGS = {
7
9
  version: 1,
8
- enabled: false,
10
+ enabled: true,
9
11
  providerType: "openai-compatible",
10
12
  baseUrl: "https://api.openai.com/v1",
11
13
  model: "gpt-4o-mini",
@@ -59,7 +61,10 @@ export function createTranslationService(deps) {
59
61
  state = {
60
62
  listeners: new Set(),
61
63
  seenTranscriptIds: new Set(),
62
- entries: []
64
+ entries: [],
65
+ status: "ready",
66
+ events: [],
67
+ nextSeq: 1
63
68
  };
64
69
  sessionStates.set(sessionId, state);
65
70
  }
@@ -71,40 +76,154 @@ export function createTranslationService(deps) {
71
76
  listener(message);
72
77
  }
73
78
  }
74
- async function handleTranscriptEvent(sessionId, event) {
79
+ function publishEntry(sessionId, entry) {
80
+ appendEvent(sessionId, { type: "entry", entry });
81
+ emit(sessionId, { type: "translation-entry", entry });
82
+ }
83
+ function publishStatus(sessionId, status) {
75
84
  const state = getState(sessionId);
76
- if (state.seenTranscriptIds.has(event.id)) {
85
+ state.status = status;
86
+ appendEvent(sessionId, { type: "status", status });
87
+ emit(sessionId, { type: "translation-status", status });
88
+ }
89
+ function publishError(sessionId, message, id) {
90
+ const state = getState(sessionId);
91
+ state.status = "failed";
92
+ appendEvent(sessionId, { type: "error", id, message });
93
+ emit(sessionId, { type: "translation-error", id, message });
94
+ }
95
+ function appendEvent(sessionId, input) {
96
+ const state = getState(sessionId);
97
+ const event = {
98
+ ...input,
99
+ seq: state.nextSeq++,
100
+ createdAt: now()
101
+ };
102
+ state.events.push(event);
103
+ void persistEvents(state);
104
+ return event;
105
+ }
106
+ async function prepareCache(input) {
107
+ const state = getState(input.sessionId);
108
+ state.repoRoot = input.repoRoot;
109
+ state.taskSlug = input.taskSlug;
110
+ state.role = input.role;
111
+ if (!deps.fs || !deps.projectService) {
112
+ return state;
113
+ }
114
+ const config = await deps.projectService.loadConfig(input.repoRoot);
115
+ const cachePath = getTranslationCachePath(input.repoRoot, config.stateRoot, input.taskSlug, input.role, input.sessionId);
116
+ state.cachePath = cachePath;
117
+ if (!state.cacheLoaded) {
118
+ await loadCachedEvents(state);
119
+ state.cacheLoaded = true;
120
+ }
121
+ await deps.fs.ensureDir(path.dirname(cachePath));
122
+ return state;
123
+ }
124
+ async function loadCachedEvents(state) {
125
+ if (!deps.fs || !state.cachePath || state.events.length > 0 || !(await deps.fs.pathExists(state.cachePath))) {
77
126
  return;
78
127
  }
79
- state.seenTranscriptIds.add(event.id);
80
- if (event.kind === "text") {
81
- state.lastAssistantText = event.text;
128
+ const text = await deps.fs.readText(state.cachePath);
129
+ const events = [];
130
+ for (const line of text.split("\n")) {
131
+ if (!line.trim()) {
132
+ continue;
133
+ }
134
+ try {
135
+ events.push(JSON.parse(line));
136
+ }
137
+ catch {
138
+ // Ignore corrupt cache lines; transcript tailing remains the source of truth.
139
+ }
140
+ }
141
+ state.events = events.sort((left, right) => left.seq - right.seq);
142
+ state.nextSeq = Math.max(state.nextSeq, ...state.events.map((event) => event.seq + 1), 1);
143
+ for (const event of state.events) {
144
+ if (event.type === "entry") {
145
+ state.entries = upsertEntry(state.entries, event.entry);
146
+ }
147
+ else if (event.type === "status") {
148
+ state.status = event.status;
149
+ }
150
+ else if (event.type === "error") {
151
+ state.status = "failed";
152
+ }
82
153
  }
83
- const { settings } = await loadConfig();
84
- if (!settings.enabled || !settings.translateOutput) {
154
+ }
155
+ async function persistEvents(state) {
156
+ if (!deps.fs || !state.cachePath) {
85
157
  return;
86
158
  }
87
- if (event.kind === "text") {
88
- await processClaudeOutputText(sessionId, event.text, event.id);
159
+ const write = async () => {
160
+ const text = state.events.map((event) => JSON.stringify(event)).join("\n");
161
+ await deps.fs.writeText(state.cachePath, text ? `${text}\n` : "");
162
+ };
163
+ state.persistChain = (state.persistChain ?? Promise.resolve()).catch(() => undefined).then(write);
164
+ await state.persistChain;
165
+ }
166
+ async function compactEventsBefore(state, nextCursor) {
167
+ const normalizedCursor = Math.max(1, Math.floor(nextCursor));
168
+ const beforeCount = state.events.length;
169
+ state.events = state.events.filter((event) => event.seq >= normalizedCursor);
170
+ if (beforeCount !== state.events.length) {
171
+ await persistEvents(state);
172
+ }
173
+ }
174
+ function startTranscriptTail(roleSession) {
175
+ const state = getState(roleSession.id);
176
+ if (state.unsubscribeTranscript) {
89
177
  return;
90
178
  }
91
- if (event.kind === "question" || event.kind === "todo" || event.kind === "agent") {
92
- await processClaudeOutputText(sessionId, formatStructuredTranscriptEvent(event), event.id);
179
+ const replaySince = getTranscriptReplaySince(roleSession);
180
+ state.unsubscribeTranscript = deps.transcripts.subscribeToRoleSession(roleSession, (event) => {
181
+ void handleTranscriptEvent(roleSession.id, event).catch((error) => {
182
+ publishError(roleSession.id, error instanceof Error ? error.message : "Translation failed.");
183
+ });
184
+ }, {
185
+ onError(error) {
186
+ publishError(roleSession.id, error.message);
187
+ },
188
+ onPoll(checkedAt) {
189
+ emit(roleSession.id, { type: "translation-poll", checkedAt });
190
+ },
191
+ replaySince
192
+ });
193
+ }
194
+ async function handleTranscriptEvent(sessionId, event) {
195
+ const state = getState(sessionId);
196
+ if (state.seenTranscriptIds.has(event.id)) {
93
197
  return;
94
198
  }
95
- if (event.kind === "tool_use" || event.kind === "tool_result") {
96
- await pushPreservedTranscriptEntry(sessionId, event.id, formatRawTranscriptEvent(event));
199
+ const config = await loadConfig();
200
+ const { settings } = config;
201
+ let displayed = false;
202
+ if (event.kind === "text") {
203
+ displayed = processClaudeOutputText(sessionId, event.text, config, event.id);
204
+ if (displayed) {
205
+ state.lastAssistantText = event.text;
206
+ }
207
+ }
208
+ else if (event.kind === "question" || event.kind === "todo" || event.kind === "agent") {
209
+ displayed = processClaudeOutputText(sessionId, formatStructuredTranscriptEvent(event), config, event.id);
210
+ }
211
+ else if (event.kind === "tool_use" || event.kind === "tool_result") {
212
+ displayed = pushPreservedTranscriptEntry(sessionId, event.id, formatRawTranscriptEvent(event), settings);
213
+ }
214
+ if (displayed) {
215
+ state.seenTranscriptIds.add(event.id);
97
216
  }
98
217
  }
99
- async function processClaudeOutputText(sessionId, rawText, entryId) {
218
+ function processClaudeOutputText(sessionId, rawText, config, entryId) {
100
219
  const session = deps.runtime.getSession(sessionId);
101
220
  const roleSession = deps.sessionRegistry.get(sessionId);
102
221
  if (!session && !roleSession) {
103
- return;
222
+ return false;
104
223
  }
105
- const { settings, secrets } = await loadConfig();
224
+ const { settings, secrets } = config;
106
225
  if (!rawText.trim()) {
107
- return;
226
+ return false;
108
227
  }
109
228
  const text = rawText;
110
229
  const baseEntry = {
@@ -123,8 +242,8 @@ export function createTranslationService(deps) {
123
242
  };
124
243
  pushEntry(sessionId, baseEntry);
125
244
  const queue = queues.getQueue(sessionId);
126
- await queue.enqueue(async () => {
127
- emit(sessionId, { type: "translation-status", status: "translating" });
245
+ void queue.enqueue(async () => {
246
+ publishStatus(sessionId, "translating");
128
247
  try {
129
248
  const prompt = buildTranslationPrompt({
130
249
  direction: "cc-output-to-user",
@@ -147,7 +266,7 @@ export function createTranslationService(deps) {
147
266
  };
148
267
  replaceEntry(sessionId, completed);
149
268
  getState(sessionId).lastAssistantText = text;
150
- emit(sessionId, { type: "translation-status", status: "ready" });
269
+ publishStatus(sessionId, "ready");
151
270
  }
152
271
  catch (error) {
153
272
  const failed = {
@@ -157,43 +276,43 @@ export function createTranslationService(deps) {
157
276
  completedAt: now()
158
277
  };
159
278
  replaceEntry(sessionId, failed);
160
- emit(sessionId, { type: "translation-status", status: "failed" });
279
+ publishStatus(sessionId, "failed");
161
280
  }
281
+ }).catch((error) => {
282
+ publishError(sessionId, error instanceof Error ? error.message : "Translation failed.");
162
283
  });
284
+ return true;
163
285
  }
164
286
  function pushEntry(sessionId, entry) {
165
287
  getState(sessionId).entries.push(entry);
166
- emit(sessionId, { type: "translation-entry", entry });
288
+ publishEntry(sessionId, entry);
167
289
  }
168
- async function pushPreservedTranscriptEntry(sessionId, entryId, sourceText) {
290
+ function pushPreservedTranscriptEntry(sessionId, entryId, sourceText, settings) {
169
291
  const session = deps.runtime.getSession(sessionId);
170
292
  const roleSession = deps.sessionRegistry.get(sessionId);
171
293
  if (!session && !roleSession) {
172
- return;
294
+ return false;
173
295
  }
174
- const { settings } = await loadConfig();
175
- const queue = queues.getQueue(sessionId);
176
- await queue.enqueue(async () => {
177
- const entry = createEntry({
178
- taskSlug: roleSession?.taskSlug ?? session.taskSlug,
179
- role: roleSession?.role ?? session.role,
180
- direction: "cc-output-to-user",
181
- sourceKind: "tool-output",
182
- sourceText,
183
- settings,
184
- status: "preserved",
185
- contextUsed: false,
186
- id: entryId,
187
- translatedText: sourceText,
188
- completedAt: now()
189
- });
190
- pushEntry(sessionId, entry);
296
+ const entry = createEntry({
297
+ taskSlug: roleSession?.taskSlug ?? session.taskSlug,
298
+ role: roleSession?.role ?? session.role,
299
+ direction: "cc-output-to-user",
300
+ sourceKind: "tool-output",
301
+ sourceText,
302
+ settings,
303
+ status: "preserved",
304
+ contextUsed: false,
305
+ id: entryId,
306
+ translatedText: sourceText,
307
+ completedAt: now()
191
308
  });
309
+ pushEntry(sessionId, entry);
310
+ return true;
192
311
  }
193
312
  function replaceEntry(sessionId, entry) {
194
313
  const state = getState(sessionId);
195
314
  state.entries = state.entries.map((current) => current.id === entry.id ? entry : current);
196
- emit(sessionId, { type: "translation-entry", entry });
315
+ publishEntry(sessionId, entry);
197
316
  }
198
317
  function createEntry(input) {
199
318
  return {
@@ -214,6 +333,23 @@ export function createTranslationService(deps) {
214
333
  model: input.settings.model
215
334
  };
216
335
  }
336
+ async function stopSessionInternal(sessionId, options = {}) {
337
+ const state = sessionStates.get(sessionId);
338
+ if (!state) {
339
+ return;
340
+ }
341
+ if (state.unsubscribeTranscript) {
342
+ state.unsubscribeTranscript();
343
+ state.unsubscribeTranscript = undefined;
344
+ }
345
+ queues.clearQueue(sessionId);
346
+ if (options.clearCache && state.cachePath && deps.fs?.removePath) {
347
+ await deps.fs.removePath(state.cachePath, { force: true });
348
+ state.events = [];
349
+ state.entries = [];
350
+ state.nextSeq = 1;
351
+ }
352
+ }
217
353
  return {
218
354
  async getSettings() {
219
355
  const { settings, secrets } = await loadConfig();
@@ -239,15 +375,46 @@ export function createTranslationService(deps) {
239
375
  const { settings, secrets } = await loadConfig();
240
376
  return deps.provider.testConnection(settings, secrets);
241
377
  },
242
- async translateUserInput(input) {
243
- const { settings, secrets } = await loadConfig();
244
- if (!settings.enabled || !settings.translateUserInput) {
378
+ async startSession(input) {
379
+ const roleSession = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.role);
380
+ if (!roleSession || roleSession.status !== "running") {
245
381
  throw new VcmError({
246
- code: "TRANSLATION_DISABLED",
247
- message: "Translation input is disabled.",
382
+ code: "SESSION_NOT_RUNNING",
383
+ message: `${input.role} session is not running.`,
248
384
  statusCode: 409
249
385
  });
250
386
  }
387
+ const state = await prepareCache({
388
+ repoRoot: input.taskRepoRoot ?? input.repoRoot,
389
+ taskSlug: input.taskSlug,
390
+ role: input.role,
391
+ sessionId: roleSession.id
392
+ });
393
+ startTranscriptTail(roleSession);
394
+ return {
395
+ sessionId: roleSession.id,
396
+ status: state.status,
397
+ nextCursor: 1
398
+ };
399
+ },
400
+ async pollSessionEvents(sessionId, after, limit = 200) {
401
+ const state = getState(sessionId);
402
+ const cursor = Number.isFinite(after) ? Math.max(1, Math.floor(after)) : 1;
403
+ const maxEvents = Math.min(Math.max(1, Math.floor(limit)), 500);
404
+ await compactEventsBefore(state, cursor);
405
+ const events = state.events
406
+ .filter((event) => event.seq >= cursor)
407
+ .slice(0, maxEvents);
408
+ const nextCursor = events.length > 0 ? (events.at(-1)?.seq ?? cursor) + 1 : cursor;
409
+ return {
410
+ sessionId,
411
+ status: state.status,
412
+ nextCursor,
413
+ events
414
+ };
415
+ },
416
+ async translateUserInput(input) {
417
+ const { settings, secrets } = await loadConfig();
251
418
  if (!input.text.trim()) {
252
419
  throw new VcmError({
253
420
  code: "TRANSLATION_INPUT_EMPTY",
@@ -256,6 +423,14 @@ export function createTranslationService(deps) {
256
423
  });
257
424
  }
258
425
  const roleSession = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.role);
426
+ if (roleSession) {
427
+ await prepareCache({
428
+ repoRoot: input.taskRepoRoot ?? input.repoRoot,
429
+ taskSlug: input.taskSlug,
430
+ role: input.role,
431
+ sessionId: roleSession.id
432
+ });
433
+ }
259
434
  const sessionState = roleSession ? getState(roleSession.id) : undefined;
260
435
  const contextText = settings.contextEnabled && input.useContext !== false
261
436
  ? sessionState?.lastAssistantText
@@ -357,40 +532,40 @@ export function createTranslationService(deps) {
357
532
  });
358
533
  }
359
534
  else {
360
- const replaySince = getTranscriptReplaySince(roleSession);
361
- state.unsubscribeTranscript = deps.transcripts.subscribeToRoleSession(roleSession, (event) => {
362
- void handleTranscriptEvent(sessionId, event).catch((error) => {
363
- emit(sessionId, {
364
- type: "translation-error",
365
- message: error instanceof Error ? error.message : "Translation failed."
366
- });
367
- });
368
- }, {
369
- onError(error) {
370
- emit(sessionId, {
371
- type: "translation-error",
372
- message: error.message
373
- });
374
- },
375
- replaySince
376
- });
535
+ startTranscriptTail(roleSession);
377
536
  }
378
537
  }
379
538
  void loadConfig().then(({ settings }) => {
380
- listener({ type: "translation-status", status: settings.enabled ? "ready" : "paused" });
539
+ listener({ type: "translation-status", status: "ready" });
381
540
  });
382
541
  return () => {
383
542
  state.listeners.delete(listener);
384
- if (state.listeners.size === 0 && state.unsubscribeTranscript) {
385
- state.unsubscribeTranscript();
386
- state.unsubscribeTranscript = undefined;
387
- }
388
543
  };
389
544
  },
390
- clearSession(sessionId) {
545
+ async clearSession(sessionId) {
391
546
  const state = getState(sessionId);
392
547
  state.entries = [];
548
+ state.events = [];
549
+ state.nextSeq = 1;
393
550
  queues.clearQueue(sessionId);
551
+ await persistEvents(state);
552
+ },
553
+ async stopSession(sessionId, options = {}) {
554
+ await stopSessionInternal(sessionId, options);
555
+ },
556
+ async stopTask(repoRoot, taskSlug, options = {}) {
557
+ for (const [sessionId, state] of sessionStates) {
558
+ if (state.repoRoot === repoRoot && state.taskSlug === taskSlug) {
559
+ await stopSessionInternal(sessionId, options);
560
+ }
561
+ }
562
+ if (options.clearCache && deps.fs?.removePath && deps.projectService) {
563
+ const config = await deps.projectService.loadConfig(repoRoot);
564
+ await deps.fs.removePath(path.join(repoRoot, config.stateRoot, "translation", taskSlug), {
565
+ recursive: true,
566
+ force: true
567
+ });
568
+ }
394
569
  },
395
570
  async retryTranslation(sessionId, translationId) {
396
571
  const state = getState(sessionId);
@@ -409,7 +584,8 @@ export function createTranslationService(deps) {
409
584
  statusCode: 400
410
585
  });
411
586
  }
412
- await processClaudeOutputText(sessionId, original.sourceText);
587
+ const config = await loadConfig();
588
+ processClaudeOutputText(sessionId, original.sourceText, config);
413
589
  return state.entries[state.entries.length - 1] ?? original;
414
590
  }
415
591
  };
@@ -422,12 +598,9 @@ export function createTranslationService(deps) {
422
598
  statusCode: 409
423
599
  });
424
600
  }
425
- deps.runtime.write(record.id, formatTerminalSubmit(text));
601
+ await submitTerminalInput(deps.runtime, record.id, text);
426
602
  }
427
603
  }
428
- export function formatTerminalSubmit(text) {
429
- return `${text.replace(/[\r\n]+$/g, "")}\r`;
430
- }
431
604
  function getTranscriptReplaySince(roleSession) {
432
605
  const rawTimestamp = roleSession.startedAt ?? roleSession.updatedAt;
433
606
  const timestampMs = Date.parse(rawTimestamp);
@@ -485,14 +658,28 @@ function formatUnknown(value) {
485
658
  return String(value);
486
659
  }
487
660
  }
661
+ function upsertEntry(entries, entry) {
662
+ const index = entries.findIndex((current) => current.id === entry.id);
663
+ if (index === -1) {
664
+ return [...entries, entry];
665
+ }
666
+ return entries.map((current) => current.id === entry.id ? entry : current);
667
+ }
668
+ function getTranslationCachePath(repoRoot, stateRoot, taskSlug, role, sessionId) {
669
+ return path.join(repoRoot, stateRoot, "translation", taskSlug, role, `${sessionId}.jsonl`);
670
+ }
488
671
  function normalizeSettings(input) {
489
672
  const { apiKey: _apiKey, ...settings } = input;
490
673
  return {
491
674
  ...DEFAULT_SETTINGS,
492
675
  ...settings,
493
676
  version: 1,
677
+ enabled: true,
494
678
  providerType: "openai-compatible",
495
679
  workingLanguage: "en",
680
+ inputMode: "review-before-send",
681
+ translateOutput: true,
682
+ translateUserInput: true,
496
683
  requestTimeoutMs: clampNumber(input.requestTimeoutMs, 3000, 120000, DEFAULT_SETTINGS.requestTimeoutMs),
497
684
  temperature: clampNumber(input.temperature, 0, 1, DEFAULT_SETTINGS.temperature),
498
685
  prompts: normalizePromptMap(input.prompts)
@@ -1,9 +1,6 @@
1
1
  export function renderGitignoreHarnessRules() {
2
2
  return [
3
3
  "# VCM local app state, task metadata, session records, and task worktrees.",
4
- ".ai/vcm/",
5
- "",
6
- "# Legacy VCM local state from early versions. Keep ignored during migration.",
7
- ".vcm/"
4
+ ".ai/vcm/"
8
5
  ].join("\n");
9
6
  }
@@ -0,0 +1 @@
1
+ export const THEME_MODES = ["system", "light", "dark"];