vibe-coding-master 0.0.9 → 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.
@@ -8,8 +8,13 @@ export function createTaskService(deps) {
8
8
  assertValidTaskSlug(input.taskSlug);
9
9
  const config = await deps.projectService.loadConfig(repoRoot);
10
10
  const taskPath = getTaskPath(repoRoot, config.stateRoot, input.taskSlug);
11
- const branch = `feature/${input.taskSlug}`;
12
- const worktreePath = getTaskWorktreePath(repoRoot, config.stateRoot, input.taskSlug);
11
+ const shouldCreateWorktree = input.createWorktree !== false;
12
+ const taskBranch = shouldCreateWorktree
13
+ ? `feature/${input.taskSlug}`
14
+ : await deps.git.getCurrentBranch(repoRoot);
15
+ const worktreePath = shouldCreateWorktree
16
+ ? getTaskWorktreePath(repoRoot, config.stateRoot, input.taskSlug)
17
+ : undefined;
13
18
  if (await deps.fs.pathExists(taskPath)) {
14
19
  throw new VcmError({
15
20
  code: "TASK_EXISTS",
@@ -17,22 +22,6 @@ export function createTaskService(deps) {
17
22
  statusCode: 409
18
23
  });
19
24
  }
20
- if (await deps.git.branchExists(repoRoot, branch)) {
21
- throw new VcmError({
22
- code: "TASK_BRANCH_EXISTS",
23
- message: `Task branch already exists: ${branch}`,
24
- statusCode: 409,
25
- hint: "Choose a different task name or clean up the existing branch."
26
- });
27
- }
28
- if (await deps.fs.pathExists(worktreePath)) {
29
- throw new VcmError({
30
- code: "TASK_WORKTREE_EXISTS",
31
- message: `Task worktree already exists: ${worktreePath}`,
32
- statusCode: 409,
33
- hint: "Choose a different task name or clean up the existing worktree."
34
- });
35
- }
36
25
  if (!(await deps.git.isIgnored(repoRoot, `${config.stateRoot}/tasks/.probe`))) {
37
26
  throw new VcmError({
38
27
  code: "VCM_STATE_NOT_IGNORED",
@@ -41,23 +30,42 @@ export function createTaskService(deps) {
41
30
  hint: "Apply VCM Harness first so .gitignore contains the VCM managed block."
42
31
  });
43
32
  }
44
- const baseStatus = await deps.git.getStatusPorcelain(repoRoot);
45
- if (baseStatus.trim()) {
46
- throw new VcmError({
47
- code: "BASE_REPO_DIRTY",
48
- message: "The connected repository has uncommitted changes.",
49
- statusCode: 409,
50
- hint: "Commit, stash, or discard base repository changes before creating a task worktree."
33
+ if (shouldCreateWorktree && worktreePath) {
34
+ if (await deps.git.branchExists(repoRoot, taskBranch)) {
35
+ throw new VcmError({
36
+ code: "TASK_BRANCH_EXISTS",
37
+ message: `Task branch already exists: ${taskBranch}`,
38
+ statusCode: 409,
39
+ hint: "Choose a different task name or clean up the existing branch."
40
+ });
41
+ }
42
+ if (await deps.fs.pathExists(worktreePath)) {
43
+ throw new VcmError({
44
+ code: "TASK_WORKTREE_EXISTS",
45
+ message: `Task worktree already exists: ${worktreePath}`,
46
+ statusCode: 409,
47
+ hint: "Choose a different task name or clean up the existing worktree."
48
+ });
49
+ }
50
+ const baseStatus = await deps.git.getStatusPorcelain(repoRoot);
51
+ if (baseStatus.trim()) {
52
+ throw new VcmError({
53
+ code: "BASE_REPO_DIRTY",
54
+ message: "The connected repository has uncommitted changes.",
55
+ statusCode: 409,
56
+ hint: "Commit, stash, or discard base repository changes before creating a task worktree."
57
+ });
58
+ }
59
+ await deps.fs.ensureDir(path.dirname(worktreePath));
60
+ await deps.git.createWorktree({
61
+ repoRoot,
62
+ branch: taskBranch,
63
+ worktreePath,
64
+ baseRef: "HEAD"
51
65
  });
52
66
  }
53
- await deps.fs.ensureDir(path.dirname(worktreePath));
54
- await deps.git.createWorktree({
55
- repoRoot,
56
- branch,
57
- worktreePath,
58
- baseRef: "HEAD"
59
- });
60
67
  const timestamp = now();
68
+ const taskRepoRoot = worktreePath ?? repoRoot;
61
69
  const task = {
62
70
  version: 1,
63
71
  taskSlug: input.taskSlug,
@@ -66,19 +74,20 @@ export function createTaskService(deps) {
66
74
  updatedAt: timestamp,
67
75
  repoRoot,
68
76
  worktreePath,
69
- branch,
77
+ branch: taskBranch,
70
78
  handoffDir: path.posix.join(config.handoffRoot, input.taskSlug),
71
79
  status: "created",
72
80
  specPath: input.specPath,
73
81
  cleanupStatus: "active"
74
82
  };
83
+ await ensureTaskRuntimeStateDirs(deps.fs, taskRepoRoot, config.stateRoot);
75
84
  await deps.artifactService.ensureHandoffStructure({
76
- repoRoot: worktreePath,
85
+ repoRoot: taskRepoRoot,
77
86
  taskSlug: input.taskSlug,
78
87
  handoffDir: task.handoffDir
79
88
  });
80
89
  await deps.artifactService.createArtifactTemplates({
81
- repoRoot: worktreePath,
90
+ repoRoot: taskRepoRoot,
82
91
  taskSlug: input.taskSlug,
83
92
  handoffDir: task.handoffDir
84
93
  });
@@ -136,30 +145,23 @@ export function createTaskService(deps) {
136
145
  }
137
146
  const config = await deps.projectService.loadConfig(repoRoot);
138
147
  const task = await this.loadTask(repoRoot, taskSlug);
148
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
149
+ const statePaths = getTaskStatePaths(repoRoot, taskRepoRoot, config.stateRoot, taskSlug);
139
150
  const removedStatePaths = [];
140
151
  const cleanedAt = now();
141
152
  if (task.worktreePath) {
142
153
  assertTaskWorktreePath(repoRoot, config.stateRoot, task.worktreePath);
143
- const status = await deps.git.getStatusPorcelain(task.worktreePath);
144
- if (status.trim() && !options.force) {
145
- throw new VcmError({
146
- code: "TASK_WORKTREE_DIRTY",
147
- message: `Task worktree has uncommitted changes: ${task.worktreePath}`,
148
- statusCode: 409,
149
- hint: "Commit, stash, or discard the task worktree changes before cleanup, or retry with force."
150
- });
151
- }
152
- await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force });
153
- }
154
- for (const statePath of getTaskStatePaths(repoRoot, config.stateRoot, taskSlug)) {
155
- await deps.fs.removePath(statePath, { force: true });
156
- removedStatePaths.push(statePath);
154
+ await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force ?? true });
157
155
  }
158
156
  let deletedBranch;
159
- if (options.deleteBranch) {
160
- await deps.git.deleteBranch(repoRoot, task.branch, { force: options.forceDeleteBranch });
157
+ if (task.worktreePath && (options.deleteBranch ?? true)) {
158
+ await deps.git.deleteBranch(repoRoot, task.branch, { force: options.forceDeleteBranch ?? true });
161
159
  deletedBranch = task.branch;
162
160
  }
161
+ for (const statePath of statePaths) {
162
+ await deps.fs.removePath(statePath, { recursive: true, force: true });
163
+ removedStatePaths.push(statePath);
164
+ }
163
165
  return {
164
166
  taskSlug,
165
167
  removedWorktreePath: task.worktreePath,
@@ -179,12 +181,19 @@ function getTaskPath(repoRoot, stateRoot, taskSlug) {
179
181
  function getTaskWorktreePath(repoRoot, stateRoot, taskSlug) {
180
182
  return path.join(repoRoot, stateRoot, "worktrees", taskSlug);
181
183
  }
182
- function getTaskStatePaths(repoRoot, stateRoot, taskSlug) {
184
+ async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
185
+ await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "sessions"));
186
+ await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "messages"));
187
+ await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "orchestration"));
188
+ await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "translation"));
189
+ }
190
+ function getTaskStatePaths(baseRepoRoot, taskRepoRoot, stateRoot, taskSlug) {
183
191
  return [
184
- path.join(repoRoot, stateRoot, "tasks", `${taskSlug}.json`),
185
- path.join(repoRoot, stateRoot, "sessions", `${taskSlug}.json`),
186
- path.join(repoRoot, stateRoot, "messages", `${taskSlug}.jsonl`),
187
- path.join(repoRoot, stateRoot, "orchestration", `${taskSlug}.json`)
192
+ path.join(baseRepoRoot, stateRoot, "tasks", `${taskSlug}.json`),
193
+ path.join(taskRepoRoot, stateRoot, "sessions", `${taskSlug}.json`),
194
+ path.join(taskRepoRoot, stateRoot, "messages", `${taskSlug}.jsonl`),
195
+ path.join(taskRepoRoot, stateRoot, "orchestration", `${taskSlug}.json`),
196
+ path.join(taskRepoRoot, stateRoot, "translation", taskSlug)
188
197
  ];
189
198
  }
190
199
  function assertTaskWorktreePath(repoRoot, stateRoot, worktreePath) {
@@ -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)
@@ -0,0 +1 @@
1
+ export const THEME_MODES = ["system", "light", "dark"];