@tiflis-io/tiflis-code-workstation 0.3.5 → 0.3.7
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/dist/main.js +1842 -219
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./chunk-JSN52PLR.js";
|
|
6
6
|
|
|
7
7
|
// src/main.ts
|
|
8
|
-
import { randomUUID as
|
|
8
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
9
9
|
|
|
10
10
|
// src/app.ts
|
|
11
11
|
import Fastify from "fastify";
|
|
@@ -129,7 +129,14 @@ var EnvSchema = z.object({
|
|
|
129
129
|
/** Terminal output buffer size (number of messages, in-memory only, does not survive restarts) */
|
|
130
130
|
TERMINAL_OUTPUT_BUFFER_SIZE: z.coerce.number().default(100),
|
|
131
131
|
// Legacy (fallback for STT/TTS if specific keys not set)
|
|
132
|
-
OPENAI_API_KEY: z.string().optional()
|
|
132
|
+
OPENAI_API_KEY: z.string().optional(),
|
|
133
|
+
// ─────────────────────────────────────────────────────────────
|
|
134
|
+
// Mock Mode Configuration (for screenshot automation)
|
|
135
|
+
// ─────────────────────────────────────────────────────────────
|
|
136
|
+
/** Enable mock mode for screenshot automation tests */
|
|
137
|
+
MOCK_MODE: z.string().transform((val) => val?.toLowerCase() === "true").default("false"),
|
|
138
|
+
/** Path to mock fixtures directory (defaults to built-in fixtures) */
|
|
139
|
+
MOCK_FIXTURES_PATH: z.string().optional()
|
|
133
140
|
});
|
|
134
141
|
function parseAgentAliases() {
|
|
135
142
|
const aliases = /* @__PURE__ */ new Map();
|
|
@@ -433,14 +440,21 @@ function getAgentConfig(agentName) {
|
|
|
433
440
|
}
|
|
434
441
|
function getWorkstationVersion() {
|
|
435
442
|
try {
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
443
|
+
const __filename2 = fileURLToPath(import.meta.url);
|
|
444
|
+
const __dirname2 = dirname(__filename2);
|
|
445
|
+
const possiblePaths = [
|
|
446
|
+
join2(__dirname2, "../package.json"),
|
|
447
|
+
join2(__dirname2, "../../package.json")
|
|
448
|
+
];
|
|
449
|
+
for (const packageJsonPath of possiblePaths) {
|
|
450
|
+
try {
|
|
451
|
+
const packageJsonContent = readFileSync(packageJsonPath, "utf-8");
|
|
452
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
453
|
+
if (packageJson.name === "@tiflis-io/tiflis-code-workstation" && typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
454
|
+
return packageJson.version;
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
444
458
|
}
|
|
445
459
|
return "0.0.0";
|
|
446
460
|
} catch {
|
|
@@ -2195,6 +2209,186 @@ var FileSystemWorkspaceDiscovery = class {
|
|
|
2195
2209
|
worktrees: []
|
|
2196
2210
|
};
|
|
2197
2211
|
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Gets current branch and uncommitted changes status for a project.
|
|
2214
|
+
*/
|
|
2215
|
+
async getBranchStatus(workspace, project) {
|
|
2216
|
+
const projectPath = join4(this.workspacesRoot, workspace, project);
|
|
2217
|
+
if (!await this.isGitRepository(projectPath)) {
|
|
2218
|
+
throw new Error(`Project "${project}" is not a git repository`);
|
|
2219
|
+
}
|
|
2220
|
+
try {
|
|
2221
|
+
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
2222
|
+
cwd: projectPath,
|
|
2223
|
+
encoding: "utf-8"
|
|
2224
|
+
}).trim();
|
|
2225
|
+
const statusOutput = execSync("git status --porcelain", {
|
|
2226
|
+
cwd: projectPath,
|
|
2227
|
+
encoding: "utf-8"
|
|
2228
|
+
});
|
|
2229
|
+
const uncommittedChanges = statusOutput.trim().split("\n").filter((line) => line.length > 0);
|
|
2230
|
+
let aheadCommits = 0;
|
|
2231
|
+
const mainBranches = ["main", "master"];
|
|
2232
|
+
for (const mainBranch of mainBranches) {
|
|
2233
|
+
try {
|
|
2234
|
+
aheadCommits = parseInt(execSync(`git rev-list --count ${mainBranch}..${currentBranch}`, {
|
|
2235
|
+
cwd: projectPath,
|
|
2236
|
+
encoding: "utf-8"
|
|
2237
|
+
}).trim(), 10);
|
|
2238
|
+
break;
|
|
2239
|
+
} catch {
|
|
2240
|
+
continue;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
return {
|
|
2244
|
+
currentBranch,
|
|
2245
|
+
uncommittedChanges,
|
|
2246
|
+
aheadCommits,
|
|
2247
|
+
isClean: uncommittedChanges.length === 0
|
|
2248
|
+
};
|
|
2249
|
+
} catch {
|
|
2250
|
+
throw new Error("Failed to get branch status");
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* Merges source branch into target branch with safety checks.
|
|
2255
|
+
*/
|
|
2256
|
+
async mergeBranch(workspace, project, sourceBranch, targetBranch = "main", options = {}) {
|
|
2257
|
+
const projectPath = join4(this.workspacesRoot, workspace, project);
|
|
2258
|
+
if (!await this.isGitRepository(projectPath)) {
|
|
2259
|
+
throw new Error(`Project "${project}" is not a git repository`);
|
|
2260
|
+
}
|
|
2261
|
+
try {
|
|
2262
|
+
if (!options.skipPreCheck) {
|
|
2263
|
+
const status = await this.getBranchStatus(workspace, project);
|
|
2264
|
+
if (!status.isClean) {
|
|
2265
|
+
return {
|
|
2266
|
+
success: false,
|
|
2267
|
+
message: `Cannot merge: uncommitted changes exist in ${status.currentBranch}`
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
execSync(`git checkout "${targetBranch}"`, { cwd: projectPath });
|
|
2272
|
+
try {
|
|
2273
|
+
execSync(`git pull origin "${targetBranch}"`, { cwd: projectPath });
|
|
2274
|
+
} catch {
|
|
2275
|
+
console.warn(`Failed to pull ${targetBranch} from remote, continuing with local merge`);
|
|
2276
|
+
}
|
|
2277
|
+
try {
|
|
2278
|
+
execSync(`git merge "${sourceBranch}"`, { cwd: projectPath });
|
|
2279
|
+
} catch (error) {
|
|
2280
|
+
const conflicts = execSync("git diff --name-only --diff-filter=U", {
|
|
2281
|
+
cwd: projectPath,
|
|
2282
|
+
encoding: "utf-8"
|
|
2283
|
+
}).trim().split("\n").filter((f) => f.length > 0);
|
|
2284
|
+
return {
|
|
2285
|
+
success: false,
|
|
2286
|
+
message: `Merge conflicts in files: ${conflicts.join(", ")}`,
|
|
2287
|
+
conflicts
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
if (options.pushAfter) {
|
|
2291
|
+
try {
|
|
2292
|
+
execSync(`git push origin "${targetBranch}"`, { cwd: projectPath });
|
|
2293
|
+
} catch {
|
|
2294
|
+
console.warn(`Failed to push ${targetBranch} to remote`);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
return {
|
|
2298
|
+
success: true,
|
|
2299
|
+
message: `Successfully merged "${sourceBranch}" into "${targetBranch}"`
|
|
2300
|
+
};
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
return {
|
|
2303
|
+
success: false,
|
|
2304
|
+
message: `Merge failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* Checks if branch is merged into target branch.
|
|
2310
|
+
*/
|
|
2311
|
+
async isBranchMerged(workspace, project, branch, targetBranch) {
|
|
2312
|
+
const projectPath = join4(this.workspacesRoot, workspace, project);
|
|
2313
|
+
if (!await this.isGitRepository(projectPath)) {
|
|
2314
|
+
return false;
|
|
2315
|
+
}
|
|
2316
|
+
try {
|
|
2317
|
+
const mergeBase = execSync(`git merge-base "${targetBranch}" "${branch}"`, {
|
|
2318
|
+
cwd: projectPath,
|
|
2319
|
+
encoding: "utf-8"
|
|
2320
|
+
}).trim();
|
|
2321
|
+
const branchHead = execSync(`git rev-parse "${branch}"`, {
|
|
2322
|
+
cwd: projectPath,
|
|
2323
|
+
encoding: "utf-8"
|
|
2324
|
+
}).trim();
|
|
2325
|
+
return mergeBase === branchHead;
|
|
2326
|
+
} catch {
|
|
2327
|
+
return false;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Cleans up worktree and safely deletes the branch if merged.
|
|
2332
|
+
*/
|
|
2333
|
+
async cleanupWorktreeAndBranch(workspace, project, branch) {
|
|
2334
|
+
const projectPath = join4(this.workspacesRoot, workspace, project);
|
|
2335
|
+
try {
|
|
2336
|
+
let branchDeleted = false;
|
|
2337
|
+
await this.removeWorktree(workspace, project, branch);
|
|
2338
|
+
const mainBranches = ["main", "master"];
|
|
2339
|
+
for (const mainBranch of mainBranches) {
|
|
2340
|
+
try {
|
|
2341
|
+
const isMerged = await this.isBranchMerged(workspace, project, branch, mainBranch);
|
|
2342
|
+
if (isMerged) {
|
|
2343
|
+
execSync(`git branch -d "${branch}"`, { cwd: projectPath });
|
|
2344
|
+
branchDeleted = true;
|
|
2345
|
+
break;
|
|
2346
|
+
}
|
|
2347
|
+
} catch {
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
return {
|
|
2352
|
+
success: true,
|
|
2353
|
+
message: `Cleaned up worktree for "${branch}"${branchDeleted ? " and deleted merged branch" : ""}`,
|
|
2354
|
+
branchDeleted
|
|
2355
|
+
};
|
|
2356
|
+
} catch (error) {
|
|
2357
|
+
return {
|
|
2358
|
+
success: false,
|
|
2359
|
+
message: `Cleanup failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2360
|
+
branchDeleted: false
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* Lists mergeable branches with their status.
|
|
2366
|
+
*/
|
|
2367
|
+
async listMergeableBranches(workspace, project) {
|
|
2368
|
+
const worktrees = await this.listWorktrees(workspace, project);
|
|
2369
|
+
const mergeableBranches = [];
|
|
2370
|
+
for (const worktree of worktrees) {
|
|
2371
|
+
if (worktree.branch === "main" || worktree.branch === "master") continue;
|
|
2372
|
+
try {
|
|
2373
|
+
const [isMerged, hasChanges, aheadCommits] = await Promise.all([
|
|
2374
|
+
this.isBranchMerged(workspace, project, worktree.branch, "main"),
|
|
2375
|
+
this.getBranchStatus(workspace, project).then((status) => !status.isClean),
|
|
2376
|
+
this.getBranchStatus(workspace, project).then((status) => status.aheadCommits)
|
|
2377
|
+
]);
|
|
2378
|
+
mergeableBranches.push({
|
|
2379
|
+
branch: worktree.branch,
|
|
2380
|
+
path: worktree.path,
|
|
2381
|
+
isMerged: await this.isBranchMerged(workspace, project, worktree.branch, "main"),
|
|
2382
|
+
hasUncommittedChanges: hasChanges,
|
|
2383
|
+
canCleanup: isMerged && !hasChanges,
|
|
2384
|
+
aheadCommits
|
|
2385
|
+
});
|
|
2386
|
+
} catch {
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return mergeableBranches;
|
|
2391
|
+
}
|
|
2198
2392
|
};
|
|
2199
2393
|
|
|
2200
2394
|
// src/infrastructure/terminal/pty-manager.ts
|
|
@@ -2522,10 +2716,79 @@ function isTerminalSession(session) {
|
|
|
2522
2716
|
return session.type === "terminal";
|
|
2523
2717
|
}
|
|
2524
2718
|
|
|
2525
|
-
// src/infrastructure/
|
|
2719
|
+
// src/infrastructure/shell/shell-env.ts
|
|
2720
|
+
import { execSync as execSync2 } from "child_process";
|
|
2721
|
+
var cachedShellEnv = null;
|
|
2526
2722
|
function getDefaultShell() {
|
|
2527
2723
|
return process.env.SHELL ?? "/bin/bash";
|
|
2528
2724
|
}
|
|
2725
|
+
function getShellEnv() {
|
|
2726
|
+
if (cachedShellEnv) {
|
|
2727
|
+
return cachedShellEnv;
|
|
2728
|
+
}
|
|
2729
|
+
const shell = getDefaultShell();
|
|
2730
|
+
const isZsh = shell.includes("zsh");
|
|
2731
|
+
const isBash = shell.includes("bash");
|
|
2732
|
+
let envOutput;
|
|
2733
|
+
try {
|
|
2734
|
+
if (isZsh) {
|
|
2735
|
+
envOutput = execSync2(`${shell} -i -l -c 'env -0'`, {
|
|
2736
|
+
encoding: "utf-8",
|
|
2737
|
+
timeout: 5e3,
|
|
2738
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2739
|
+
// 10MB buffer for large environments
|
|
2740
|
+
env: {
|
|
2741
|
+
...process.env,
|
|
2742
|
+
// Prevent zsh from printing extra output
|
|
2743
|
+
PROMPT_EOL_MARK: ""
|
|
2744
|
+
},
|
|
2745
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2746
|
+
// Capture stderr to ignore it
|
|
2747
|
+
});
|
|
2748
|
+
} else if (isBash) {
|
|
2749
|
+
envOutput = execSync2(`${shell} --login -i -c 'env -0'`, {
|
|
2750
|
+
encoding: "utf-8",
|
|
2751
|
+
timeout: 5e3,
|
|
2752
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2753
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2754
|
+
});
|
|
2755
|
+
} else {
|
|
2756
|
+
envOutput = execSync2(`${shell} -l -c 'env -0'`, {
|
|
2757
|
+
encoding: "utf-8",
|
|
2758
|
+
timeout: 5e3,
|
|
2759
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2760
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2763
|
+
const shellEnv = {};
|
|
2764
|
+
for (const entry of envOutput.split("\0")) {
|
|
2765
|
+
if (!entry) continue;
|
|
2766
|
+
const eqIndex = entry.indexOf("=");
|
|
2767
|
+
if (eqIndex > 0) {
|
|
2768
|
+
const key = entry.slice(0, eqIndex);
|
|
2769
|
+
const value = entry.slice(eqIndex + 1);
|
|
2770
|
+
shellEnv[key] = value;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
cachedShellEnv = {
|
|
2774
|
+
...process.env,
|
|
2775
|
+
...shellEnv
|
|
2776
|
+
};
|
|
2777
|
+
return cachedShellEnv;
|
|
2778
|
+
} catch (error) {
|
|
2779
|
+
console.warn(
|
|
2780
|
+
"Failed to retrieve shell environment, using process.env:",
|
|
2781
|
+
error instanceof Error ? error.message : String(error)
|
|
2782
|
+
);
|
|
2783
|
+
cachedShellEnv = { ...process.env };
|
|
2784
|
+
return cachedShellEnv;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
// src/infrastructure/terminal/pty-manager.ts
|
|
2789
|
+
function getDefaultShell2() {
|
|
2790
|
+
return process.env.SHELL ?? "/bin/bash";
|
|
2791
|
+
}
|
|
2529
2792
|
var PtyManager = class {
|
|
2530
2793
|
logger;
|
|
2531
2794
|
bufferSize;
|
|
@@ -2539,18 +2802,19 @@ var PtyManager = class {
|
|
|
2539
2802
|
*/
|
|
2540
2803
|
create(workingDir, cols, rows) {
|
|
2541
2804
|
const sessionId = new SessionId(nanoid(12));
|
|
2542
|
-
const shell =
|
|
2805
|
+
const shell = getDefaultShell2();
|
|
2543
2806
|
this.logger.debug(
|
|
2544
2807
|
{ sessionId: sessionId.value, workingDir, shell, cols, rows },
|
|
2545
2808
|
"Creating terminal session"
|
|
2546
2809
|
);
|
|
2810
|
+
const shellEnv = getShellEnv();
|
|
2547
2811
|
const ptyProcess = pty.spawn(shell, [], {
|
|
2548
2812
|
name: "xterm-256color",
|
|
2549
2813
|
cols,
|
|
2550
2814
|
rows,
|
|
2551
2815
|
cwd: workingDir,
|
|
2552
2816
|
env: {
|
|
2553
|
-
...
|
|
2817
|
+
...shellEnv,
|
|
2554
2818
|
TERM: "xterm-256color",
|
|
2555
2819
|
// Disable zsh partial line marker (inverse % sign on startup)
|
|
2556
2820
|
PROMPT_EOL_MARK: ""
|
|
@@ -2640,10 +2904,11 @@ var HeadlessAgentExecutor = class extends EventEmitter {
|
|
|
2640
2904
|
}
|
|
2641
2905
|
const { command, args } = this.buildCommand(prompt);
|
|
2642
2906
|
const aliasEnvVars = this.getAliasEnvVars();
|
|
2907
|
+
const shellEnv = getShellEnv();
|
|
2643
2908
|
this.subprocess = spawn2(command, args, {
|
|
2644
2909
|
cwd: this.workingDir,
|
|
2645
2910
|
env: {
|
|
2646
|
-
...
|
|
2911
|
+
...shellEnv,
|
|
2647
2912
|
// Apply alias env vars (e.g., CLAUDE_CONFIG_DIR)
|
|
2648
2913
|
...aliasEnvVars,
|
|
2649
2914
|
// Ensure proper terminal environment
|
|
@@ -3607,6 +3872,87 @@ var AgentSessionManager = class extends EventEmitter2 {
|
|
|
3607
3872
|
this.terminateSession(id);
|
|
3608
3873
|
}
|
|
3609
3874
|
}
|
|
3875
|
+
/**
|
|
3876
|
+
* Terminates sessions that are running in a specific worktree.
|
|
3877
|
+
* Returns the list of terminated session IDs.
|
|
3878
|
+
*/
|
|
3879
|
+
terminateWorktreeSessions(workspace, project, branch) {
|
|
3880
|
+
const worktreePath = `/${workspace}/${project}--${branch}`;
|
|
3881
|
+
const terminatedSessions = [];
|
|
3882
|
+
for (const [sessionId, state] of this.sessions) {
|
|
3883
|
+
const isInWorktree = state.workingDir.includes(worktreePath) || state.cliSessionId?.includes(`${project}--${branch}`) || state.workingDir.endsWith(`${project}--${branch}`);
|
|
3884
|
+
if (isInWorktree) {
|
|
3885
|
+
try {
|
|
3886
|
+
if (state.isExecuting) {
|
|
3887
|
+
this.cancelCommand(sessionId);
|
|
3888
|
+
}
|
|
3889
|
+
this.terminateSession(sessionId);
|
|
3890
|
+
terminatedSessions.push(sessionId);
|
|
3891
|
+
this.logger.info({ sessionId, workspace, project, branch }, "Terminated worktree session");
|
|
3892
|
+
} catch (error) {
|
|
3893
|
+
this.logger.error({ sessionId, error }, "Failed to terminate worktree session");
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
return terminatedSessions;
|
|
3898
|
+
}
|
|
3899
|
+
/**
|
|
3900
|
+
* Gets session summary for a specific worktree.
|
|
3901
|
+
*/
|
|
3902
|
+
getWorktreeSessionSummary(workspace, project, branch) {
|
|
3903
|
+
const worktreePath = `/${workspace}/${project}--${branch}`;
|
|
3904
|
+
const activeSessions = Array.from(this.sessions.values()).filter(
|
|
3905
|
+
(session) => session.workingDir.includes(worktreePath) || session.cliSessionId?.includes(`${project}--${branch}`) || session.workingDir.endsWith(`${project}--${branch}`)
|
|
3906
|
+
);
|
|
3907
|
+
const sessionTypes = [...new Set(activeSessions.map((s) => s.agentType))];
|
|
3908
|
+
const executingCount = activeSessions.filter((s) => s.isExecuting).length;
|
|
3909
|
+
return {
|
|
3910
|
+
activeSessions,
|
|
3911
|
+
sessionCount: activeSessions.length,
|
|
3912
|
+
sessionTypes,
|
|
3913
|
+
executingCount
|
|
3914
|
+
};
|
|
3915
|
+
}
|
|
3916
|
+
/**
|
|
3917
|
+
* Lists all sessions with their worktree information.
|
|
3918
|
+
*/
|
|
3919
|
+
listSessionsWithWorktreeInfo() {
|
|
3920
|
+
return Array.from(this.sessions.values()).map((session) => {
|
|
3921
|
+
const worktreeInfo = this.parseWorktreeInfo(session.workingDir);
|
|
3922
|
+
return {
|
|
3923
|
+
sessionId: session.sessionId,
|
|
3924
|
+
agentType: session.agentType,
|
|
3925
|
+
agentName: session.agentName,
|
|
3926
|
+
workingDir: session.workingDir,
|
|
3927
|
+
isExecuting: session.isExecuting,
|
|
3928
|
+
worktreeInfo
|
|
3929
|
+
};
|
|
3930
|
+
});
|
|
3931
|
+
}
|
|
3932
|
+
/**
|
|
3933
|
+
* Parse worktree information from a working directory path.
|
|
3934
|
+
*/
|
|
3935
|
+
parseWorktreeInfo(workingDir) {
|
|
3936
|
+
const parts = workingDir.split("/");
|
|
3937
|
+
if (parts.length < 3) {
|
|
3938
|
+
return { isWorktree: false };
|
|
3939
|
+
}
|
|
3940
|
+
const workspacesIndex = parts.findIndex((part) => part === "workspaces" || part.includes("workspace"));
|
|
3941
|
+
if (workspacesIndex === -1 || workspacesIndex + 2 >= parts.length) {
|
|
3942
|
+
return { isWorktree: false };
|
|
3943
|
+
}
|
|
3944
|
+
const workspace = parts[workspacesIndex + 1];
|
|
3945
|
+
const projectPart = parts[workspacesIndex + 2];
|
|
3946
|
+
if (!projectPart) {
|
|
3947
|
+
return { isWorktree: false };
|
|
3948
|
+
}
|
|
3949
|
+
if (projectPart.includes("--")) {
|
|
3950
|
+
const [project, branch] = projectPart.split("--");
|
|
3951
|
+
return { workspace, project, branch, isWorktree: true };
|
|
3952
|
+
} else {
|
|
3953
|
+
return { workspace, project: projectPart, isWorktree: false };
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3610
3956
|
/**
|
|
3611
3957
|
* Setup event handlers for an executor.
|
|
3612
3958
|
*/
|
|
@@ -4293,7 +4639,14 @@ var SubscriptionService = class {
|
|
|
4293
4639
|
}
|
|
4294
4640
|
const isNew = client.subscribe(session);
|
|
4295
4641
|
if (isNew) {
|
|
4296
|
-
|
|
4642
|
+
try {
|
|
4643
|
+
this.deps.subscriptionRepository.subscribe(deviceId, sessionId);
|
|
4644
|
+
} catch (error) {
|
|
4645
|
+
this.logger.debug(
|
|
4646
|
+
{ deviceId, sessionId, error },
|
|
4647
|
+
"Failed to persist subscription (may be expected in mock mode)"
|
|
4648
|
+
);
|
|
4649
|
+
}
|
|
4297
4650
|
}
|
|
4298
4651
|
let isMaster = false;
|
|
4299
4652
|
let cols;
|
|
@@ -5414,76 +5767,359 @@ var ChatHistoryService = class _ChatHistoryService {
|
|
|
5414
5767
|
);
|
|
5415
5768
|
return agentSessions;
|
|
5416
5769
|
}
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
//
|
|
5420
|
-
import { EventEmitter as EventEmitter3 } from "events";
|
|
5421
|
-
import { nanoid as nanoid3 } from "nanoid";
|
|
5422
|
-
|
|
5423
|
-
// src/domain/entities/supervisor-session.ts
|
|
5424
|
-
var SupervisorSession = class extends Session {
|
|
5425
|
-
_contextCleared = false;
|
|
5426
|
-
constructor(props) {
|
|
5427
|
-
super({ ...props, type: "supervisor" });
|
|
5428
|
-
}
|
|
5429
|
-
/**
|
|
5430
|
-
* Returns whether the context has been cleared.
|
|
5431
|
-
*/
|
|
5432
|
-
get contextCleared() {
|
|
5433
|
-
return this._contextCleared;
|
|
5434
|
-
}
|
|
5770
|
+
// ============================================================================
|
|
5771
|
+
// Mock Data Seeding (for Screenshot Automation)
|
|
5772
|
+
// ============================================================================
|
|
5435
5773
|
/**
|
|
5436
|
-
*
|
|
5774
|
+
* Seeds mock chat history for screenshot automation.
|
|
5775
|
+
* Creates realistic conversation history with voice messages, code blocks, etc.
|
|
5776
|
+
*
|
|
5777
|
+
* @param agentSessions - Object with agent session IDs and their working directories
|
|
5437
5778
|
*/
|
|
5438
|
-
|
|
5439
|
-
this.
|
|
5440
|
-
this.
|
|
5441
|
-
|
|
5779
|
+
seedMockData(agentSessions) {
|
|
5780
|
+
this.logger.info("Seeding mock chat history for screenshots...");
|
|
5781
|
+
this.seedSupervisorHistory();
|
|
5782
|
+
if (agentSessions.claude) {
|
|
5783
|
+
this.ensureAgentSession(agentSessions.claude.id, "claude", agentSessions.claude.workingDir);
|
|
5784
|
+
this.seedClaudeAgentHistory(agentSessions.claude.id);
|
|
5785
|
+
}
|
|
5786
|
+
if (agentSessions.cursor) {
|
|
5787
|
+
this.ensureAgentSession(agentSessions.cursor.id, "cursor", agentSessions.cursor.workingDir);
|
|
5788
|
+
this.seedCursorAgentHistory(agentSessions.cursor.id);
|
|
5789
|
+
}
|
|
5790
|
+
if (agentSessions.opencode) {
|
|
5791
|
+
this.ensureAgentSession(agentSessions.opencode.id, "opencode", agentSessions.opencode.workingDir);
|
|
5792
|
+
this.seedOpenCodeAgentHistory(agentSessions.opencode.id);
|
|
5793
|
+
}
|
|
5794
|
+
this.logger.info("Mock chat history seeded successfully");
|
|
5442
5795
|
}
|
|
5443
5796
|
/**
|
|
5444
|
-
*
|
|
5445
|
-
*
|
|
5797
|
+
* Ensures an agent session exists in the database.
|
|
5798
|
+
* Creates it if it doesn't exist.
|
|
5446
5799
|
*/
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5800
|
+
ensureAgentSession(sessionId, sessionType, workingDir) {
|
|
5801
|
+
try {
|
|
5802
|
+
const existing = this.sessionRepo.getById(sessionId);
|
|
5803
|
+
if (!existing) {
|
|
5804
|
+
this.sessionRepo.create({
|
|
5805
|
+
id: sessionId,
|
|
5806
|
+
type: sessionType,
|
|
5807
|
+
workingDir
|
|
5808
|
+
});
|
|
5809
|
+
this.logger.debug({ sessionId, sessionType }, "Created agent session in database for seeding");
|
|
5810
|
+
}
|
|
5811
|
+
} catch {
|
|
5450
5812
|
}
|
|
5451
|
-
this.markTerminated();
|
|
5452
|
-
return Promise.resolve();
|
|
5453
|
-
}
|
|
5454
|
-
};
|
|
5455
|
-
|
|
5456
|
-
// src/infrastructure/persistence/in-memory-session-manager.ts
|
|
5457
|
-
var InMemorySessionManager = class extends EventEmitter3 {
|
|
5458
|
-
sessions = /* @__PURE__ */ new Map();
|
|
5459
|
-
ptyManager;
|
|
5460
|
-
agentSessionManager;
|
|
5461
|
-
logger;
|
|
5462
|
-
supervisorSession = null;
|
|
5463
|
-
constructor(config2) {
|
|
5464
|
-
super();
|
|
5465
|
-
this.ptyManager = config2.ptyManager;
|
|
5466
|
-
this.agentSessionManager = config2.agentSessionManager;
|
|
5467
|
-
this.logger = config2.logger.child({ component: "session-manager" });
|
|
5468
|
-
this.setupAgentSessionSync();
|
|
5469
5813
|
}
|
|
5470
5814
|
/**
|
|
5471
|
-
*
|
|
5815
|
+
* Seeds Supervisor chat with a realistic voice conversation.
|
|
5472
5816
|
*/
|
|
5473
|
-
|
|
5474
|
-
this.
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5817
|
+
seedSupervisorHistory() {
|
|
5818
|
+
this.ensureSupervisorSession();
|
|
5819
|
+
const sessionId = _ChatHistoryService.SUPERVISOR_SESSION_ID;
|
|
5820
|
+
this.messageRepo.deleteBySession(sessionId);
|
|
5821
|
+
const voiceInput1 = {
|
|
5822
|
+
id: "vi-1",
|
|
5823
|
+
block_type: "voice_input",
|
|
5824
|
+
content: "Show me the available workspaces",
|
|
5825
|
+
metadata: { duration: 2.1, has_audio: true }
|
|
5826
|
+
};
|
|
5827
|
+
this.messageRepo.create({
|
|
5828
|
+
sessionId,
|
|
5829
|
+
role: "user",
|
|
5830
|
+
contentType: "transcription",
|
|
5831
|
+
content: "Show me the available workspaces",
|
|
5832
|
+
contentBlocks: JSON.stringify([voiceInput1]),
|
|
5833
|
+
isComplete: true
|
|
5480
5834
|
});
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5835
|
+
const textBlock1 = {
|
|
5836
|
+
id: "tb-1",
|
|
5837
|
+
block_type: "text",
|
|
5838
|
+
content: "I found 2 workspaces with several projects. The **work** workspace contains my-app and api-service. The **personal** workspace has your blog project. Would you like to start an agent session in any of these?"
|
|
5839
|
+
};
|
|
5840
|
+
const voiceOutput1 = {
|
|
5841
|
+
id: "vo-1",
|
|
5842
|
+
block_type: "voice_output",
|
|
5843
|
+
content: "I found 2 workspaces with several projects.",
|
|
5844
|
+
metadata: { duration: 4.2, has_audio: true }
|
|
5845
|
+
};
|
|
5846
|
+
this.messageRepo.create({
|
|
5847
|
+
sessionId,
|
|
5848
|
+
role: "assistant",
|
|
5849
|
+
contentType: "text",
|
|
5850
|
+
content: "I found 2 workspaces with several projects. The work workspace contains my-app and api-service. The personal workspace has your blog project. Would you like to start an agent session in any of these?",
|
|
5851
|
+
contentBlocks: JSON.stringify([textBlock1, voiceOutput1]),
|
|
5852
|
+
isComplete: true
|
|
5853
|
+
});
|
|
5854
|
+
const voiceInput2 = {
|
|
5855
|
+
id: "vi-2",
|
|
5856
|
+
block_type: "voice_input",
|
|
5857
|
+
content: "Start Claude on my-app",
|
|
5858
|
+
metadata: { duration: 1.8, has_audio: true }
|
|
5859
|
+
};
|
|
5860
|
+
this.messageRepo.create({
|
|
5861
|
+
sessionId,
|
|
5862
|
+
role: "user",
|
|
5863
|
+
contentType: "transcription",
|
|
5864
|
+
content: "Start Claude on my-app",
|
|
5865
|
+
contentBlocks: JSON.stringify([voiceInput2]),
|
|
5866
|
+
isComplete: true
|
|
5867
|
+
});
|
|
5868
|
+
const textBlock2 = {
|
|
5869
|
+
id: "tb-2",
|
|
5870
|
+
block_type: "text",
|
|
5871
|
+
content: "I've started a new Claude Code session in **work/my-app**. You can find it in the sidebar under Agent Sessions. The session is ready for your commands!"
|
|
5872
|
+
};
|
|
5873
|
+
const voiceOutput2 = {
|
|
5874
|
+
id: "vo-2",
|
|
5875
|
+
block_type: "voice_output",
|
|
5876
|
+
content: "I've started a new Claude Code session in work/my-app.",
|
|
5877
|
+
metadata: { duration: 3.5, has_audio: true }
|
|
5878
|
+
};
|
|
5879
|
+
this.messageRepo.create({
|
|
5880
|
+
sessionId,
|
|
5881
|
+
role: "assistant",
|
|
5882
|
+
contentType: "text",
|
|
5883
|
+
content: "I've started a new Claude Code session in work/my-app. You can find it in the sidebar under Agent Sessions. The session is ready for your commands!",
|
|
5884
|
+
contentBlocks: JSON.stringify([textBlock2, voiceOutput2]),
|
|
5885
|
+
isComplete: true
|
|
5886
|
+
});
|
|
5887
|
+
this.logger.debug("Seeded Supervisor history with voice conversation");
|
|
5888
|
+
}
|
|
5889
|
+
/**
|
|
5890
|
+
* Seeds Claude agent chat with code examples and tool use.
|
|
5891
|
+
*/
|
|
5892
|
+
seedClaudeAgentHistory(sessionId) {
|
|
5893
|
+
this.messageRepo.deleteBySession(sessionId);
|
|
5894
|
+
const userVoiceBlock = {
|
|
5895
|
+
id: "vi-claude-1",
|
|
5896
|
+
block_type: "voice_input",
|
|
5897
|
+
content: "Add a health check endpoint to the API",
|
|
5898
|
+
metadata: {
|
|
5899
|
+
duration: 2.3,
|
|
5900
|
+
has_audio: true
|
|
5901
|
+
}
|
|
5902
|
+
};
|
|
5903
|
+
this.messageRepo.create({
|
|
5904
|
+
sessionId,
|
|
5905
|
+
role: "user",
|
|
5906
|
+
contentType: "audio",
|
|
5907
|
+
content: "Add a health check endpoint to the API",
|
|
5908
|
+
contentBlocks: JSON.stringify([userVoiceBlock]),
|
|
5909
|
+
isComplete: true
|
|
5910
|
+
});
|
|
5911
|
+
const thinkingBlock = {
|
|
5912
|
+
id: "think-1",
|
|
5913
|
+
block_type: "thinking",
|
|
5914
|
+
content: "I'll add a simple health check endpoint that returns the server status and version information."
|
|
5915
|
+
};
|
|
5916
|
+
const toolBlock = {
|
|
5917
|
+
id: "tool-1",
|
|
5918
|
+
block_type: "tool",
|
|
5919
|
+
content: "Edit",
|
|
5920
|
+
metadata: {
|
|
5921
|
+
tool_name: "Edit",
|
|
5922
|
+
tool_status: "completed",
|
|
5923
|
+
tool_input: JSON.stringify({ file: "src/routes/health.ts" })
|
|
5924
|
+
}
|
|
5925
|
+
};
|
|
5926
|
+
const codeBlock = {
|
|
5927
|
+
id: "code-1",
|
|
5928
|
+
block_type: "code",
|
|
5929
|
+
content: `import { Router } from 'express';
|
|
5930
|
+
|
|
5931
|
+
const router = Router();
|
|
5932
|
+
|
|
5933
|
+
router.get('/health', (req, res) => {
|
|
5934
|
+
res.json({
|
|
5935
|
+
status: 'healthy',
|
|
5936
|
+
version: process.env.npm_package_version,
|
|
5937
|
+
uptime: process.uptime()
|
|
5938
|
+
});
|
|
5939
|
+
});
|
|
5940
|
+
|
|
5941
|
+
export default router;`,
|
|
5942
|
+
metadata: { language: "typescript" }
|
|
5943
|
+
};
|
|
5944
|
+
const textBlock = {
|
|
5945
|
+
id: "text-1",
|
|
5946
|
+
block_type: "text",
|
|
5947
|
+
content: "I've added a health check endpoint at `/health` that returns the server status, version, and uptime. You can test it with `curl http://localhost:3000/health`."
|
|
5948
|
+
};
|
|
5949
|
+
this.messageRepo.create({
|
|
5950
|
+
sessionId,
|
|
5951
|
+
role: "assistant",
|
|
5952
|
+
contentType: "text",
|
|
5953
|
+
content: "I've added a health check endpoint at /health that returns the server status, version, and uptime.",
|
|
5954
|
+
contentBlocks: JSON.stringify([thinkingBlock, toolBlock, codeBlock, textBlock]),
|
|
5955
|
+
isComplete: true
|
|
5956
|
+
});
|
|
5957
|
+
this.logger.debug({ sessionId }, "Seeded Claude agent history with code example");
|
|
5958
|
+
}
|
|
5959
|
+
/**
|
|
5960
|
+
* Seeds Cursor agent chat.
|
|
5961
|
+
*/
|
|
5962
|
+
seedCursorAgentHistory(sessionId) {
|
|
5963
|
+
this.messageRepo.deleteBySession(sessionId);
|
|
5964
|
+
this.messageRepo.create({
|
|
5965
|
+
sessionId,
|
|
5966
|
+
role: "user",
|
|
5967
|
+
contentType: "text",
|
|
5968
|
+
content: "Explain the project structure",
|
|
5969
|
+
isComplete: true
|
|
5970
|
+
});
|
|
5971
|
+
const textBlock = {
|
|
5972
|
+
id: "text-cursor-1",
|
|
5973
|
+
block_type: "text",
|
|
5974
|
+
content: `This is a **Next.js portfolio site** with the following structure:
|
|
5975
|
+
|
|
5976
|
+
- \`/app\` - App router pages and layouts
|
|
5977
|
+
- \`/components\` - Reusable React components
|
|
5978
|
+
- \`/lib\` - Utility functions and helpers
|
|
5979
|
+
- \`/public\` - Static assets (images, fonts)
|
|
5980
|
+
- \`/styles\` - Global CSS and Tailwind config
|
|
5981
|
+
|
|
5982
|
+
The site uses **Tailwind CSS** for styling and **Framer Motion** for animations. Would you like me to explain any specific part in more detail?`
|
|
5983
|
+
};
|
|
5984
|
+
this.messageRepo.create({
|
|
5985
|
+
sessionId,
|
|
5986
|
+
role: "assistant",
|
|
5987
|
+
contentType: "text",
|
|
5988
|
+
content: "This is a Next.js portfolio site with app router, components, lib, public, and styles directories.",
|
|
5989
|
+
contentBlocks: JSON.stringify([textBlock]),
|
|
5990
|
+
isComplete: true
|
|
5991
|
+
});
|
|
5992
|
+
this.logger.debug({ sessionId }, "Seeded Cursor agent history");
|
|
5993
|
+
}
|
|
5994
|
+
/**
|
|
5995
|
+
* Seeds OpenCode agent chat.
|
|
5996
|
+
*/
|
|
5997
|
+
seedOpenCodeAgentHistory(sessionId) {
|
|
5998
|
+
this.messageRepo.deleteBySession(sessionId);
|
|
5999
|
+
this.messageRepo.create({
|
|
6000
|
+
sessionId,
|
|
6001
|
+
role: "user",
|
|
6002
|
+
contentType: "text",
|
|
6003
|
+
content: "Run the tests",
|
|
6004
|
+
isComplete: true
|
|
6005
|
+
});
|
|
6006
|
+
const statusBlock = {
|
|
6007
|
+
id: "status-1",
|
|
6008
|
+
block_type: "status",
|
|
6009
|
+
content: "Running tests..."
|
|
6010
|
+
};
|
|
6011
|
+
const codeBlock = {
|
|
6012
|
+
id: "code-oc-1",
|
|
6013
|
+
block_type: "code",
|
|
6014
|
+
content: `\u2713 auth.test.ts (3 tests) 120ms
|
|
6015
|
+
\u2713 api.test.ts (8 tests) 340ms
|
|
6016
|
+
\u2713 utils.test.ts (5 tests) 45ms
|
|
6017
|
+
|
|
6018
|
+
Test Files 3 passed (3)
|
|
6019
|
+
Tests 16 passed (16)
|
|
6020
|
+
Time 0.51s`,
|
|
6021
|
+
metadata: { language: "shell" }
|
|
6022
|
+
};
|
|
6023
|
+
const textBlock = {
|
|
6024
|
+
id: "text-oc-1",
|
|
6025
|
+
block_type: "text",
|
|
6026
|
+
content: "All 16 tests passed across 3 test files. The test suite completed in 0.51 seconds."
|
|
6027
|
+
};
|
|
6028
|
+
this.messageRepo.create({
|
|
6029
|
+
sessionId,
|
|
6030
|
+
role: "assistant",
|
|
6031
|
+
contentType: "text",
|
|
6032
|
+
content: "All 16 tests passed across 3 test files.",
|
|
6033
|
+
contentBlocks: JSON.stringify([statusBlock, codeBlock, textBlock]),
|
|
6034
|
+
isComplete: true
|
|
6035
|
+
});
|
|
6036
|
+
this.logger.debug({ sessionId }, "Seeded OpenCode agent history");
|
|
6037
|
+
}
|
|
6038
|
+
};
|
|
6039
|
+
|
|
6040
|
+
// src/infrastructure/persistence/in-memory-session-manager.ts
|
|
6041
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
6042
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
6043
|
+
|
|
6044
|
+
// src/domain/entities/supervisor-session.ts
|
|
6045
|
+
var SupervisorSession = class extends Session {
|
|
6046
|
+
_contextCleared = false;
|
|
6047
|
+
constructor(props) {
|
|
6048
|
+
super({ ...props, type: "supervisor" });
|
|
6049
|
+
}
|
|
6050
|
+
/**
|
|
6051
|
+
* Returns whether the context has been cleared.
|
|
6052
|
+
*/
|
|
6053
|
+
get contextCleared() {
|
|
6054
|
+
return this._contextCleared;
|
|
6055
|
+
}
|
|
6056
|
+
/**
|
|
6057
|
+
* Clears the supervisor's conversation context.
|
|
6058
|
+
*/
|
|
6059
|
+
clearContext() {
|
|
6060
|
+
this._contextCleared = true;
|
|
6061
|
+
this.recordActivity();
|
|
6062
|
+
this._contextCleared = false;
|
|
6063
|
+
}
|
|
6064
|
+
/**
|
|
6065
|
+
* Terminates the supervisor session.
|
|
6066
|
+
* Note: Supervisor is typically not terminated, but this is provided for completeness.
|
|
6067
|
+
*/
|
|
6068
|
+
terminate() {
|
|
6069
|
+
if (this._status === "terminated") {
|
|
6070
|
+
return Promise.resolve();
|
|
6071
|
+
}
|
|
6072
|
+
this.markTerminated();
|
|
6073
|
+
return Promise.resolve();
|
|
6074
|
+
}
|
|
6075
|
+
};
|
|
6076
|
+
|
|
6077
|
+
// src/infrastructure/persistence/in-memory-session-manager.ts
|
|
6078
|
+
var InMemorySessionManager = class extends EventEmitter3 {
|
|
6079
|
+
sessions = /* @__PURE__ */ new Map();
|
|
6080
|
+
ptyManager;
|
|
6081
|
+
agentSessionManager;
|
|
6082
|
+
logger;
|
|
6083
|
+
supervisorSession = null;
|
|
6084
|
+
constructor(config2) {
|
|
6085
|
+
super();
|
|
6086
|
+
this.ptyManager = config2.ptyManager;
|
|
6087
|
+
this.agentSessionManager = config2.agentSessionManager;
|
|
6088
|
+
this.logger = config2.logger.child({ component: "session-manager" });
|
|
6089
|
+
this.setupAgentSessionSync();
|
|
6090
|
+
}
|
|
6091
|
+
/**
|
|
6092
|
+
* Sets up event listeners to sync agent session state.
|
|
6093
|
+
*/
|
|
6094
|
+
setupAgentSessionSync() {
|
|
6095
|
+
this.agentSessionManager.on("sessionCreated", (state) => {
|
|
6096
|
+
if (!this.sessions.has(state.sessionId)) {
|
|
6097
|
+
const session = new AgentSession({
|
|
6098
|
+
id: new SessionId(state.sessionId),
|
|
6099
|
+
type: state.agentType,
|
|
6100
|
+
agentName: state.agentName,
|
|
6101
|
+
workingDir: state.workingDir
|
|
6102
|
+
});
|
|
6103
|
+
this.sessions.set(state.sessionId, session);
|
|
6104
|
+
this.logger.debug(
|
|
6105
|
+
{ sessionId: state.sessionId, agentType: state.agentType },
|
|
6106
|
+
"Agent session registered from external creation"
|
|
6107
|
+
);
|
|
6108
|
+
}
|
|
6109
|
+
});
|
|
6110
|
+
this.agentSessionManager.on("sessionTerminated", (sessionId) => {
|
|
6111
|
+
const session = this.sessions.get(sessionId);
|
|
6112
|
+
if (session && isAgentType(session.type)) {
|
|
6113
|
+
this.sessions.delete(sessionId);
|
|
6114
|
+
this.logger.debug({ sessionId }, "Agent session removed from registry");
|
|
6115
|
+
}
|
|
6116
|
+
});
|
|
6117
|
+
this.agentSessionManager.on("cliSessionIdDiscovered", (sessionId, cliSessionId) => {
|
|
6118
|
+
const session = this.sessions.get(sessionId);
|
|
6119
|
+
if (session instanceof AgentSession) {
|
|
6120
|
+
session.setCliSessionId(cliSessionId);
|
|
6121
|
+
this.logger.debug({ sessionId, cliSessionId }, "CLI session ID updated");
|
|
6122
|
+
}
|
|
5487
6123
|
});
|
|
5488
6124
|
}
|
|
5489
6125
|
/**
|
|
@@ -5777,7 +6413,7 @@ Path: ${project.path}`;
|
|
|
5777
6413
|
// src/infrastructure/agents/supervisor/tools/worktree-tools.ts
|
|
5778
6414
|
import { tool as tool2 } from "@langchain/core/tools";
|
|
5779
6415
|
import { z as z4 } from "zod";
|
|
5780
|
-
function createWorktreeTools(workspaceDiscovery) {
|
|
6416
|
+
function createWorktreeTools(workspaceDiscovery, _agentSessionManager) {
|
|
5781
6417
|
const listWorktrees = tool2(
|
|
5782
6418
|
async ({ workspace, project }) => {
|
|
5783
6419
|
try {
|
|
@@ -5857,7 +6493,223 @@ ${worktrees.map((w) => `- ${w.name}: ${w.branch} (${w.path})`).join("\n")}`;
|
|
|
5857
6493
|
})
|
|
5858
6494
|
}
|
|
5859
6495
|
);
|
|
5860
|
-
|
|
6496
|
+
const branchStatus = tool2(
|
|
6497
|
+
async ({ workspace, project }) => {
|
|
6498
|
+
try {
|
|
6499
|
+
const status = await workspaceDiscovery.getBranchStatus(workspace, project);
|
|
6500
|
+
return {
|
|
6501
|
+
currentBranch: status.currentBranch,
|
|
6502
|
+
uncommittedChanges: status.uncommittedChanges,
|
|
6503
|
+
aheadCommits: status.aheadCommits,
|
|
6504
|
+
isClean: status.isClean,
|
|
6505
|
+
summary: status.isClean ? `Branch "${status.currentBranch}" is clean with ${status.aheadCommits} commits ahead of main` : `Branch "${status.currentBranch}" has ${status.uncommittedChanges.length} uncommitted changes:
|
|
6506
|
+
${status.uncommittedChanges.map((c) => ` ${c}`).join("\n")}`
|
|
6507
|
+
};
|
|
6508
|
+
} catch (error) {
|
|
6509
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
6510
|
+
}
|
|
6511
|
+
},
|
|
6512
|
+
{
|
|
6513
|
+
name: "branch_status",
|
|
6514
|
+
description: "Get current branch status including uncommitted changes and commit count ahead of main",
|
|
6515
|
+
schema: z4.object({
|
|
6516
|
+
workspace: z4.string().describe("Workspace name containing the project"),
|
|
6517
|
+
project: z4.string().describe("Project name to get branch status for")
|
|
6518
|
+
})
|
|
6519
|
+
}
|
|
6520
|
+
);
|
|
6521
|
+
const mergeBranch = tool2(
|
|
6522
|
+
async ({ workspace, project, sourceBranch, targetBranch, pushAfter, skipPreCheck }) => {
|
|
6523
|
+
try {
|
|
6524
|
+
const result = await workspaceDiscovery.mergeBranch(
|
|
6525
|
+
workspace,
|
|
6526
|
+
project,
|
|
6527
|
+
sourceBranch,
|
|
6528
|
+
targetBranch ?? "main",
|
|
6529
|
+
{ pushAfter, skipPreCheck }
|
|
6530
|
+
);
|
|
6531
|
+
return result;
|
|
6532
|
+
} catch (error) {
|
|
6533
|
+
return {
|
|
6534
|
+
success: false,
|
|
6535
|
+
message: `Merge operation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
6536
|
+
};
|
|
6537
|
+
}
|
|
6538
|
+
},
|
|
6539
|
+
{
|
|
6540
|
+
name: "merge_branch",
|
|
6541
|
+
description: "Merge source branch into target branch with safety checks and optional push to remote",
|
|
6542
|
+
schema: z4.object({
|
|
6543
|
+
workspace: z4.string().describe("Workspace name"),
|
|
6544
|
+
project: z4.string().describe("Project name"),
|
|
6545
|
+
sourceBranch: z4.string().describe("Source branch to merge from"),
|
|
6546
|
+
targetBranch: z4.string().optional().describe("Target branch (defaults to main)"),
|
|
6547
|
+
pushAfter: z4.boolean().optional().describe("Push to remote after successful merge"),
|
|
6548
|
+
skipPreCheck: z4.boolean().optional().describe("Skip pre-merge safety checks (not recommended)")
|
|
6549
|
+
})
|
|
6550
|
+
}
|
|
6551
|
+
);
|
|
6552
|
+
const listMergeableBranches = tool2(
|
|
6553
|
+
async ({ workspace, project }) => {
|
|
6554
|
+
try {
|
|
6555
|
+
const branches = await workspaceDiscovery.listMergeableBranches(workspace, project);
|
|
6556
|
+
if (branches.length === 0) {
|
|
6557
|
+
return "No feature branches found.";
|
|
6558
|
+
}
|
|
6559
|
+
const statusLines = branches.map((b) => {
|
|
6560
|
+
const status = [
|
|
6561
|
+
`Branch: ${b.branch}`,
|
|
6562
|
+
`Path: ${b.path}`,
|
|
6563
|
+
`Merged: ${b.isMerged ? "\u2713" : "\u2717"}`,
|
|
6564
|
+
`Clean: ${b.hasUncommittedChanges ? "\u2717 (has changes)" : "\u2713"}`,
|
|
6565
|
+
`Cleanup ready: ${b.canCleanup ? "\u2713" : "\u2717"}`,
|
|
6566
|
+
`Ahead commits: ${b.aheadCommits}`
|
|
6567
|
+
].join(" | ");
|
|
6568
|
+
return `- ${status}`;
|
|
6569
|
+
});
|
|
6570
|
+
const summary = `Found ${branches.length} branches. ${branches.filter((b) => b.canCleanup).length} are ready for cleanup.`;
|
|
6571
|
+
return `${summary}
|
|
6572
|
+
|
|
6573
|
+
${statusLines.join("\n")}`;
|
|
6574
|
+
} catch (error) {
|
|
6575
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
6576
|
+
}
|
|
6577
|
+
},
|
|
6578
|
+
{
|
|
6579
|
+
name: "list_mergeable_branches",
|
|
6580
|
+
description: "List all feature branches and their merge/cleanup status",
|
|
6581
|
+
schema: z4.object({
|
|
6582
|
+
workspace: z4.string().describe("Workspace name"),
|
|
6583
|
+
project: z4.string().describe("Project name")
|
|
6584
|
+
})
|
|
6585
|
+
}
|
|
6586
|
+
);
|
|
6587
|
+
const cleanupWorktree = tool2(
|
|
6588
|
+
async ({ workspace, project, branch, force }) => {
|
|
6589
|
+
try {
|
|
6590
|
+
if (!force) {
|
|
6591
|
+
const status = await workspaceDiscovery.getBranchStatus(workspace, project);
|
|
6592
|
+
if (!status.isClean && status.currentBranch === branch) {
|
|
6593
|
+
return {
|
|
6594
|
+
success: false,
|
|
6595
|
+
message: `Cannot cleanup: uncommitted changes exist in "${branch}". Use force=true to override.`,
|
|
6596
|
+
uncommittedChanges: status.uncommittedChanges
|
|
6597
|
+
};
|
|
6598
|
+
}
|
|
6599
|
+
}
|
|
6600
|
+
const result = await workspaceDiscovery.cleanupWorktreeAndBranch(
|
|
6601
|
+
workspace,
|
|
6602
|
+
project,
|
|
6603
|
+
branch
|
|
6604
|
+
);
|
|
6605
|
+
return result;
|
|
6606
|
+
} catch (error) {
|
|
6607
|
+
return {
|
|
6608
|
+
success: false,
|
|
6609
|
+
message: `Cleanup failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
6610
|
+
branchDeleted: false
|
|
6611
|
+
};
|
|
6612
|
+
}
|
|
6613
|
+
},
|
|
6614
|
+
{
|
|
6615
|
+
name: "cleanup_worktree",
|
|
6616
|
+
description: "Remove worktree and optionally delete the branch if merged",
|
|
6617
|
+
schema: z4.object({
|
|
6618
|
+
workspace: z4.string().describe("Workspace name"),
|
|
6619
|
+
project: z4.string().describe("Project name"),
|
|
6620
|
+
branch: z4.string().describe("Branch/worktree name to cleanup"),
|
|
6621
|
+
force: z4.boolean().optional().describe("Force cleanup even with uncommitted changes")
|
|
6622
|
+
})
|
|
6623
|
+
}
|
|
6624
|
+
);
|
|
6625
|
+
const completeFeature = tool2(
|
|
6626
|
+
async ({ workspace, project, featureBranch, targetBranch, skipConfirmation }) => {
|
|
6627
|
+
try {
|
|
6628
|
+
const results = [];
|
|
6629
|
+
const status = await workspaceDiscovery.getBranchStatus(workspace, project);
|
|
6630
|
+
results.push({
|
|
6631
|
+
step: "status_check",
|
|
6632
|
+
data: {
|
|
6633
|
+
currentBranch: status.currentBranch,
|
|
6634
|
+
isClean: status.isClean,
|
|
6635
|
+
uncommittedChanges: status.uncommittedChanges,
|
|
6636
|
+
aheadCommits: status.aheadCommits
|
|
6637
|
+
}
|
|
6638
|
+
});
|
|
6639
|
+
if (!status.isClean && status.currentBranch === featureBranch && !skipConfirmation) {
|
|
6640
|
+
return {
|
|
6641
|
+
success: false,
|
|
6642
|
+
message: `Cannot complete feature: uncommitted changes exist in "${featureBranch}". Commit or stash changes first.`,
|
|
6643
|
+
results
|
|
6644
|
+
};
|
|
6645
|
+
}
|
|
6646
|
+
const mergeResult = await workspaceDiscovery.mergeBranch(
|
|
6647
|
+
workspace,
|
|
6648
|
+
project,
|
|
6649
|
+
featureBranch,
|
|
6650
|
+
targetBranch ?? "main",
|
|
6651
|
+
{ pushAfter: true }
|
|
6652
|
+
);
|
|
6653
|
+
results.push({
|
|
6654
|
+
step: "merge",
|
|
6655
|
+
data: mergeResult
|
|
6656
|
+
});
|
|
6657
|
+
if (!mergeResult.success) {
|
|
6658
|
+
return {
|
|
6659
|
+
success: false,
|
|
6660
|
+
message: `Merge failed: ${mergeResult.message}`,
|
|
6661
|
+
results
|
|
6662
|
+
};
|
|
6663
|
+
}
|
|
6664
|
+
const cleanupResult = await workspaceDiscovery.cleanupWorktreeAndBranch(
|
|
6665
|
+
workspace,
|
|
6666
|
+
project,
|
|
6667
|
+
featureBranch
|
|
6668
|
+
);
|
|
6669
|
+
results.push({
|
|
6670
|
+
step: "cleanup",
|
|
6671
|
+
data: cleanupResult
|
|
6672
|
+
});
|
|
6673
|
+
return {
|
|
6674
|
+
success: true,
|
|
6675
|
+
message: `\u2705 Feature "${featureBranch}" completed successfully!
|
|
6676
|
+
|
|
6677
|
+
Steps executed:
|
|
6678
|
+
1. \u2705 Branch validated
|
|
6679
|
+
2. \u2705 Merged into ${targetBranch ?? "main"} and pushed
|
|
6680
|
+
3. \u2705 Worktree cleaned up${cleanupResult.branchDeleted ? " and branch deleted" : ""}`,
|
|
6681
|
+
results
|
|
6682
|
+
};
|
|
6683
|
+
} catch (error) {
|
|
6684
|
+
return {
|
|
6685
|
+
success: false,
|
|
6686
|
+
message: `Feature completion failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
6687
|
+
results: []
|
|
6688
|
+
};
|
|
6689
|
+
}
|
|
6690
|
+
},
|
|
6691
|
+
{
|
|
6692
|
+
name: "complete_feature",
|
|
6693
|
+
description: "Complete workflow: merge feature branch into main and cleanup worktree/branch",
|
|
6694
|
+
schema: z4.object({
|
|
6695
|
+
workspace: z4.string().describe("Workspace name"),
|
|
6696
|
+
project: z4.string().describe("Project name"),
|
|
6697
|
+
featureBranch: z4.string().describe("Feature branch name to complete"),
|
|
6698
|
+
targetBranch: z4.string().optional().describe("Target branch (defaults to main)"),
|
|
6699
|
+
skipConfirmation: z4.boolean().optional().describe("Skip safety checks (not recommended)")
|
|
6700
|
+
})
|
|
6701
|
+
}
|
|
6702
|
+
);
|
|
6703
|
+
return [
|
|
6704
|
+
listWorktrees,
|
|
6705
|
+
createWorktree,
|
|
6706
|
+
removeWorktree,
|
|
6707
|
+
branchStatus,
|
|
6708
|
+
mergeBranch,
|
|
6709
|
+
listMergeableBranches,
|
|
6710
|
+
cleanupWorktree,
|
|
6711
|
+
completeFeature
|
|
6712
|
+
];
|
|
5861
6713
|
}
|
|
5862
6714
|
|
|
5863
6715
|
// src/infrastructure/agents/supervisor/tools/session-tools.ts
|
|
@@ -6148,7 +7000,96 @@ Messages: ${agentState.messages.length}`;
|
|
|
6148
7000
|
})
|
|
6149
7001
|
}
|
|
6150
7002
|
);
|
|
6151
|
-
|
|
7003
|
+
const listSessionsWithWorktrees = tool3(
|
|
7004
|
+
() => {
|
|
7005
|
+
try {
|
|
7006
|
+
const sessions2 = agentSessionManager.listSessionsWithWorktreeInfo();
|
|
7007
|
+
if (sessions2.length === 0) {
|
|
7008
|
+
return "No active sessions.";
|
|
7009
|
+
}
|
|
7010
|
+
const sessionLines = sessions2.map((s) => {
|
|
7011
|
+
const worktreeInfo = s.worktreeInfo ? s.worktreeInfo.isWorktree ? ` (worktree: ${s.worktreeInfo.workspace}/${s.worktreeInfo.project}--${s.worktreeInfo.branch})` : ` (main: ${s.worktreeInfo.workspace}/${s.worktreeInfo.project})` : "";
|
|
7012
|
+
const executing = s.isExecuting ? " [executing]" : "";
|
|
7013
|
+
return `- [${s.agentType}] ${s.sessionId}${executing}${worktreeInfo}
|
|
7014
|
+
Working dir: ${s.workingDir}`;
|
|
7015
|
+
});
|
|
7016
|
+
return `Active sessions:
|
|
7017
|
+
${sessionLines.join("\n")}`;
|
|
7018
|
+
} catch (error) {
|
|
7019
|
+
return `Error listing sessions: ${error instanceof Error ? error.message : String(error)}`;
|
|
7020
|
+
}
|
|
7021
|
+
},
|
|
7022
|
+
{
|
|
7023
|
+
name: "list_sessions_with_worktrees",
|
|
7024
|
+
description: "Lists all active sessions with worktree information for branch management",
|
|
7025
|
+
schema: z5.object({})
|
|
7026
|
+
}
|
|
7027
|
+
);
|
|
7028
|
+
const getWorktreeSessionSummary = tool3(
|
|
7029
|
+
({ workspace, project, branch }) => {
|
|
7030
|
+
try {
|
|
7031
|
+
const summary = agentSessionManager.getWorktreeSessionSummary(workspace, project, branch);
|
|
7032
|
+
if (summary.sessionCount === 0) {
|
|
7033
|
+
return `No active sessions in worktree "${workspace}/${project}--${branch}".`;
|
|
7034
|
+
}
|
|
7035
|
+
const sessionDetails = summary.activeSessions.map(
|
|
7036
|
+
(s) => `- [${s.agentType}] ${s.sessionId} (${s.isExecuting ? "executing" : "idle"})
|
|
7037
|
+
Created: ${new Date(s.createdAt).toISOString()}`
|
|
7038
|
+
).join("\n");
|
|
7039
|
+
return `Worktree "${workspace}/${project}--${branch}" has ${summary.sessionCount} active session(s):
|
|
7040
|
+
${sessionDetails}
|
|
7041
|
+
|
|
7042
|
+
Session types: ${summary.sessionTypes.join(", ")}
|
|
7043
|
+
Executing: ${summary.executingCount}`;
|
|
7044
|
+
} catch (error) {
|
|
7045
|
+
return `Error getting worktree session summary: ${error instanceof Error ? error.message : String(error)}`;
|
|
7046
|
+
}
|
|
7047
|
+
},
|
|
7048
|
+
{
|
|
7049
|
+
name: "get_worktree_session_summary",
|
|
7050
|
+
description: "Gets detailed session information for a specific worktree",
|
|
7051
|
+
schema: z5.object({
|
|
7052
|
+
workspace: z5.string().describe("Workspace name"),
|
|
7053
|
+
project: z5.string().describe("Project name"),
|
|
7054
|
+
branch: z5.string().describe("Branch/worktree name")
|
|
7055
|
+
})
|
|
7056
|
+
}
|
|
7057
|
+
);
|
|
7058
|
+
const terminateWorktreeSessions = tool3(
|
|
7059
|
+
({ workspace, project, branch }) => {
|
|
7060
|
+
try {
|
|
7061
|
+
const terminatedSessions = agentSessionManager.terminateWorktreeSessions(workspace, project, branch);
|
|
7062
|
+
if (terminatedSessions.length === 0) {
|
|
7063
|
+
return `No active sessions to terminate in worktree "${workspace}/${project}--${branch}".`;
|
|
7064
|
+
}
|
|
7065
|
+
return `Terminated ${terminatedSessions.length} session(s) in worktree "${workspace}/${project}--${branch}":
|
|
7066
|
+
${terminatedSessions.map((id) => ` - ${id}`).join("\n")}`;
|
|
7067
|
+
} catch (error) {
|
|
7068
|
+
return `Error terminating worktree sessions: ${error instanceof Error ? error.message : String(error)}`;
|
|
7069
|
+
}
|
|
7070
|
+
},
|
|
7071
|
+
{
|
|
7072
|
+
name: "terminate_worktree_sessions",
|
|
7073
|
+
description: "Terminates all active sessions in a specific worktree",
|
|
7074
|
+
schema: z5.object({
|
|
7075
|
+
workspace: z5.string().describe("Workspace name"),
|
|
7076
|
+
project: z5.string().describe("Project name"),
|
|
7077
|
+
branch: z5.string().describe("Branch/worktree name")
|
|
7078
|
+
})
|
|
7079
|
+
}
|
|
7080
|
+
);
|
|
7081
|
+
return [
|
|
7082
|
+
listSessions,
|
|
7083
|
+
listAvailableAgents,
|
|
7084
|
+
createAgentSession,
|
|
7085
|
+
createTerminalSession,
|
|
7086
|
+
terminateSession,
|
|
7087
|
+
terminateAllSessions,
|
|
7088
|
+
getSessionInfo,
|
|
7089
|
+
listSessionsWithWorktrees,
|
|
7090
|
+
getWorktreeSessionSummary,
|
|
7091
|
+
terminateWorktreeSessions
|
|
7092
|
+
];
|
|
6152
7093
|
}
|
|
6153
7094
|
|
|
6154
7095
|
// src/infrastructure/agents/supervisor/tools/filesystem-tools.ts
|
|
@@ -6280,7 +7221,7 @@ var SupervisorAgent = class extends EventEmitter4 {
|
|
|
6280
7221
|
const llm = this.createLLM(env);
|
|
6281
7222
|
const tools = [
|
|
6282
7223
|
...createWorkspaceTools(config2.workspaceDiscovery),
|
|
6283
|
-
...createWorktreeTools(config2.workspaceDiscovery),
|
|
7224
|
+
...createWorktreeTools(config2.workspaceDiscovery, config2.agentSessionManager),
|
|
6284
7225
|
...createSessionTools(
|
|
6285
7226
|
config2.sessionManager,
|
|
6286
7227
|
config2.agentSessionManager,
|
|
@@ -6495,179 +7436,719 @@ var SupervisorAgent = class extends EventEmitter4 {
|
|
|
6495
7436
|
);
|
|
6496
7437
|
return false;
|
|
6497
7438
|
}
|
|
6498
|
-
this.logger.info(
|
|
6499
|
-
{ isProcessingCommand: this.isProcessingCommand, isExecuting: this.isExecuting, timeSinceStart },
|
|
6500
|
-
"Cancelling supervisor execution"
|
|
6501
|
-
);
|
|
6502
|
-
this.isCancelled = true;
|
|
6503
|
-
this.isExecuting = false;
|
|
6504
|
-
this.isProcessingCommand = false;
|
|
6505
|
-
if (this.abortController) {
|
|
6506
|
-
this.abortController.abort();
|
|
7439
|
+
this.logger.info(
|
|
7440
|
+
{ isProcessingCommand: this.isProcessingCommand, isExecuting: this.isExecuting, timeSinceStart },
|
|
7441
|
+
"Cancelling supervisor execution"
|
|
7442
|
+
);
|
|
7443
|
+
this.isCancelled = true;
|
|
7444
|
+
this.isExecuting = false;
|
|
7445
|
+
this.isProcessingCommand = false;
|
|
7446
|
+
if (this.abortController) {
|
|
7447
|
+
this.abortController.abort();
|
|
7448
|
+
}
|
|
7449
|
+
return true;
|
|
7450
|
+
}
|
|
7451
|
+
/**
|
|
7452
|
+
* Check if execution was cancelled.
|
|
7453
|
+
* Used by main.ts to filter out any late-arriving blocks.
|
|
7454
|
+
*/
|
|
7455
|
+
wasCancelled() {
|
|
7456
|
+
return this.isCancelled;
|
|
7457
|
+
}
|
|
7458
|
+
/**
|
|
7459
|
+
* Starts command processing (before STT/LLM execution).
|
|
7460
|
+
* Returns an AbortController that can be used to cancel STT and other operations.
|
|
7461
|
+
*/
|
|
7462
|
+
startProcessing() {
|
|
7463
|
+
this.abortController = new AbortController();
|
|
7464
|
+
this.isProcessingCommand = true;
|
|
7465
|
+
this.isCancelled = false;
|
|
7466
|
+
this.logger.debug("Started command processing");
|
|
7467
|
+
return this.abortController;
|
|
7468
|
+
}
|
|
7469
|
+
/**
|
|
7470
|
+
* Checks if command processing is active (STT or LLM execution).
|
|
7471
|
+
*/
|
|
7472
|
+
isProcessing() {
|
|
7473
|
+
return this.isProcessingCommand || this.isExecuting;
|
|
7474
|
+
}
|
|
7475
|
+
/**
|
|
7476
|
+
* Ends command processing (called after completion or error, not after cancel).
|
|
7477
|
+
*/
|
|
7478
|
+
endProcessing() {
|
|
7479
|
+
this.isProcessingCommand = false;
|
|
7480
|
+
this.logger.debug("Ended command processing");
|
|
7481
|
+
}
|
|
7482
|
+
/**
|
|
7483
|
+
* Clears global conversation history.
|
|
7484
|
+
* Also resets cancellation state to allow new commands.
|
|
7485
|
+
*/
|
|
7486
|
+
clearHistory() {
|
|
7487
|
+
this.conversationHistory = [];
|
|
7488
|
+
this.isCancelled = false;
|
|
7489
|
+
this.logger.info("Global conversation history cleared");
|
|
7490
|
+
}
|
|
7491
|
+
/**
|
|
7492
|
+
* Resets the cancellation state.
|
|
7493
|
+
* Call this before starting a new command to ensure previous cancellation doesn't affect it.
|
|
7494
|
+
*/
|
|
7495
|
+
resetCancellationState() {
|
|
7496
|
+
this.isCancelled = false;
|
|
7497
|
+
}
|
|
7498
|
+
/**
|
|
7499
|
+
* Restores global conversation history from persistent storage.
|
|
7500
|
+
* Called on startup to sync in-memory cache with database.
|
|
7501
|
+
*/
|
|
7502
|
+
restoreHistory(history) {
|
|
7503
|
+
if (history.length === 0) return;
|
|
7504
|
+
this.conversationHistory = history.slice(-20);
|
|
7505
|
+
this.logger.debug({ messageCount: this.conversationHistory.length }, "Global conversation history restored");
|
|
7506
|
+
}
|
|
7507
|
+
/**
|
|
7508
|
+
* Builds the system message for the agent.
|
|
7509
|
+
*/
|
|
7510
|
+
buildSystemMessage() {
|
|
7511
|
+
const systemPrompt = `You are the Supervisor Agent for Tiflis Code, a workstation management system.
|
|
7512
|
+
|
|
7513
|
+
Your role is to help users:
|
|
7514
|
+
1. **Discover workspaces and projects** - List available workspaces and projects
|
|
7515
|
+
2. **Manage git worktrees** - Create, list, and remove worktrees for parallel development
|
|
7516
|
+
3. **Manage sessions** - Create and terminate agent sessions (Cursor, Claude, OpenCode) and terminal sessions
|
|
7517
|
+
4. **Navigate the file system** - List directories and read files
|
|
7518
|
+
5. **Complete feature workflows** - Merge branches, clean up worktrees, and manage related sessions
|
|
7519
|
+
|
|
7520
|
+
## CRITICAL: Always Use Tools - Never Be Lazy
|
|
7521
|
+
|
|
7522
|
+
**YOU MUST ALWAYS call tools to execute user requests. NEVER be lazy or skip actions based on memory or previous context.**
|
|
7523
|
+
|
|
7524
|
+
### Mandatory Tool Usage Rules:
|
|
7525
|
+
|
|
7526
|
+
1. **ALWAYS call tools for fresh data** - When user asks about workspaces, projects, sessions, or any system state:
|
|
7527
|
+
- ALWAYS call the appropriate tool (list_workspaces, list_projects, list_sessions, etc.)
|
|
7528
|
+
- NEVER respond from memory or previous conversation context
|
|
7529
|
+
- System state changes constantly - what was true before may not be true now
|
|
7530
|
+
|
|
7531
|
+
2. **ALWAYS execute requested actions** - When user asks to create, terminate, or modify something:
|
|
7532
|
+
- ALWAYS call the tool to perform the action, even if you think it was done before
|
|
7533
|
+
- If user asks to create a session and one already exists, CREATE ANOTHER ONE (user knows what they want)
|
|
7534
|
+
- If user asks to list projects, LIST THEM NOW with a tool call (don't say "I already showed you")
|
|
7535
|
+
- NEVER refuse a direct request because "it was already done" or "nothing changed"
|
|
7536
|
+
|
|
7537
|
+
3. **User intent is paramount** - When user explicitly requests an action:
|
|
7538
|
+
- Execute it immediately without questioning or suggesting alternatives
|
|
7539
|
+
- Don't assume user made a mistake - they know what they need
|
|
7540
|
+
- Multiple sessions in the same project is a valid use case
|
|
7541
|
+
- Refreshing information is always valid
|
|
7542
|
+
|
|
7543
|
+
4. **No shortcuts** - You must:
|
|
7544
|
+
- Call list_workspaces/list_projects EVERY time user asks what workspaces/projects exist
|
|
7545
|
+
- Call list_sessions EVERY time user asks about active sessions
|
|
7546
|
+
- Call create_agent_session/create_terminal_session EVERY time user asks to create a session
|
|
7547
|
+
- Never say "based on our previous conversation" or "as I mentioned earlier" for factual data
|
|
7548
|
+
|
|
7549
|
+
## Feature Completion & Merge Workflows
|
|
7550
|
+
|
|
7551
|
+
When users ask to "complete the feature", "finish the work", "merge and clean up", or similar requests:
|
|
7552
|
+
|
|
7553
|
+
### Safety Checks First:
|
|
7554
|
+
1. **Check branch status** with \`branch_status\` - Look for uncommitted changes
|
|
7555
|
+
2. **List active sessions** with \`get_worktree_session_summary\` - Find sessions in the worktree
|
|
7556
|
+
3. **Ask for confirmation** if there are uncommitted changes or active sessions
|
|
7557
|
+
|
|
7558
|
+
### Complete Workflow with \`complete_feature\`:
|
|
7559
|
+
- Merges feature branch into main with automatic push
|
|
7560
|
+
- Cleans up the worktree and removes the branch if merged
|
|
7561
|
+
- One-command solution for feature completion
|
|
7562
|
+
|
|
7563
|
+
### Step-by-Step Alternative:
|
|
7564
|
+
1. **Handle uncommitted changes**: Commit, stash, or get user confirmation
|
|
7565
|
+
2. **Terminate sessions**: Use \`terminate_worktree_sessions\` to clean up active sessions
|
|
7566
|
+
3. **Merge branch**: Use \`merge_branch\` with pushAfter=true
|
|
7567
|
+
4. **Cleanup worktree**: Use \`cleanup_worktree\` to remove worktree directory
|
|
7568
|
+
|
|
7569
|
+
### Available Merge Tools:
|
|
7570
|
+
- **branch_status** - Check current branch state and uncommitted changes
|
|
7571
|
+
- **merge_branch** - Safe merge with conflict detection and push
|
|
7572
|
+
- **complete_feature** - Full workflow (merge + cleanup + push)
|
|
7573
|
+
- **cleanup_worktree** - Remove worktree and delete merged branch
|
|
7574
|
+
- **list_mergeable_branches** - Show all branches and their cleanup eligibility
|
|
7575
|
+
- **get_worktree_session_summary** - List sessions in a specific worktree
|
|
7576
|
+
- **terminate_worktree_sessions** - End all sessions in a worktree
|
|
7577
|
+
|
|
7578
|
+
### Error Handling:
|
|
7579
|
+
- **Merge conflicts**: Report conflicting files and suggest manual resolution
|
|
7580
|
+
- **Uncommitted changes**: Offer to commit, stash, or force cleanup
|
|
7581
|
+
- **Active sessions**: List sessions and ask for termination confirmation
|
|
7582
|
+
- **Failed pushes**: Continue with local merge, warn about remote sync
|
|
7583
|
+
|
|
7584
|
+
## Guidelines:
|
|
7585
|
+
- Be concise and helpful
|
|
7586
|
+
- Use tools to gather information before responding
|
|
7587
|
+
- When creating sessions, always confirm the workspace and project first
|
|
7588
|
+
- For ambiguous requests, ask clarifying questions
|
|
7589
|
+
- Format responses for terminal display (avoid markdown links)
|
|
7590
|
+
- ALWAYS prioritize safety - check before deleting/merging
|
|
7591
|
+
|
|
7592
|
+
## Session Types:
|
|
7593
|
+
- **cursor** - Cursor AI agent for code assistance
|
|
7594
|
+
- **claude** - Claude Code CLI for AI coding
|
|
7595
|
+
- **opencode** - OpenCode AI agent
|
|
7596
|
+
- **terminal** - Shell terminal for direct commands
|
|
7597
|
+
|
|
7598
|
+
## Creating Agent Sessions:
|
|
7599
|
+
When creating agent sessions, by default use the main project directory (main or master branch) unless the user explicitly requests a specific worktree or branch:
|
|
7600
|
+
- **Default behavior**: Omit the \`worktree\` parameter to create session on the main/master branch (project root directory)
|
|
7601
|
+
- **Specific worktree**: Only specify \`worktree\` when the user explicitly asks for a feature branch worktree (NOT the main branch)
|
|
7602
|
+
- **IMPORTANT**: When \`list_worktrees\` shows a worktree named "main" with \`isMain: true\`, this represents the project root directory. Do NOT pass \`worktree: "main"\` - instead, omit the worktree parameter entirely to use the project root.
|
|
7603
|
+
|
|
7604
|
+
## Worktree Management:
|
|
7605
|
+
Worktrees allow working on multiple branches simultaneously in separate directories.
|
|
7606
|
+
- **Branch naming**: Use conventional format \`<type>/<name>\` where \`<name>\` is lower-kebab-case. Types: \`feature\`, \`fix\`, \`refactor\`, \`docs\`, \`chore\`. Examples: \`feature/user-auth\`, \`fix/keyboard-layout\`, \`refactor/websocket-handler\`
|
|
7607
|
+
- **Directory pattern**: \`project--branch-name\` (slashes replaced with dashes, e.g., \`my-app--feature-user-auth\`)
|
|
7608
|
+
- **Creating worktrees**: Use \`create_worktree\` tool with:
|
|
7609
|
+
- \`createNewBranch: true\` \u2014 Creates a NEW branch and worktree (most common for new features)
|
|
7610
|
+
- \`createNewBranch: false\` \u2014 Checks out an EXISTING branch into a worktree
|
|
7611
|
+
- \`baseBranch\` \u2014 Optional starting point for new branches (defaults to HEAD, commonly "main")`;
|
|
7612
|
+
return [new HumanMessage(`[System Instructions]
|
|
7613
|
+
${systemPrompt}
|
|
7614
|
+
[End Instructions]`)];
|
|
7615
|
+
}
|
|
7616
|
+
/**
|
|
7617
|
+
* Builds messages from conversation history.
|
|
7618
|
+
*/
|
|
7619
|
+
buildHistoryMessages(history) {
|
|
7620
|
+
return history.map(
|
|
7621
|
+
(entry) => entry.role === "user" ? new HumanMessage(entry.content) : new AIMessage(entry.content)
|
|
7622
|
+
);
|
|
7623
|
+
}
|
|
7624
|
+
/**
|
|
7625
|
+
* Gets global conversation history.
|
|
7626
|
+
*/
|
|
7627
|
+
getConversationHistory() {
|
|
7628
|
+
return this.conversationHistory;
|
|
7629
|
+
}
|
|
7630
|
+
/**
|
|
7631
|
+
* Adds an entry to global conversation history.
|
|
7632
|
+
*/
|
|
7633
|
+
addToHistory(role, content) {
|
|
7634
|
+
this.conversationHistory.push({ role, content });
|
|
7635
|
+
if (this.conversationHistory.length > 20) {
|
|
7636
|
+
this.conversationHistory.splice(0, this.conversationHistory.length - 20);
|
|
7637
|
+
}
|
|
7638
|
+
}
|
|
7639
|
+
};
|
|
7640
|
+
|
|
7641
|
+
// src/infrastructure/mock/mock-supervisor-agent.ts
|
|
7642
|
+
import { EventEmitter as EventEmitter5 } from "events";
|
|
7643
|
+
|
|
7644
|
+
// src/infrastructure/mock/fixture-loader.ts
|
|
7645
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
7646
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
7647
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7648
|
+
var __filename = fileURLToPath2(import.meta.url);
|
|
7649
|
+
var __dirname = dirname2(__filename);
|
|
7650
|
+
var DEFAULT_FIXTURES_PATH = join7(__dirname, "fixtures");
|
|
7651
|
+
var fixtureCache = /* @__PURE__ */ new Map();
|
|
7652
|
+
function loadFixture(name, customPath) {
|
|
7653
|
+
const cacheKey = `${customPath ?? "default"}:${name}`;
|
|
7654
|
+
if (fixtureCache.has(cacheKey)) {
|
|
7655
|
+
return fixtureCache.get(cacheKey);
|
|
7656
|
+
}
|
|
7657
|
+
const fixturesDir = customPath ?? DEFAULT_FIXTURES_PATH;
|
|
7658
|
+
const filePath = join7(fixturesDir, `${name}.json`);
|
|
7659
|
+
if (!existsSync3(filePath)) {
|
|
7660
|
+
console.warn(`[MockMode] Fixture not found: ${filePath}`);
|
|
7661
|
+
return null;
|
|
7662
|
+
}
|
|
7663
|
+
try {
|
|
7664
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
7665
|
+
const fixture = JSON.parse(content);
|
|
7666
|
+
fixtureCache.set(cacheKey, fixture);
|
|
7667
|
+
return fixture;
|
|
7668
|
+
} catch (error) {
|
|
7669
|
+
console.error(`[MockMode] Failed to load fixture ${filePath}:`, error);
|
|
7670
|
+
return null;
|
|
7671
|
+
}
|
|
7672
|
+
}
|
|
7673
|
+
function findMatchingResponse(fixture, input) {
|
|
7674
|
+
const normalizedInput = input.toLowerCase().trim();
|
|
7675
|
+
for (const scenario of Object.values(fixture.scenarios)) {
|
|
7676
|
+
for (const trigger of scenario.triggers) {
|
|
7677
|
+
if (normalizedInput.includes(trigger.toLowerCase())) {
|
|
7678
|
+
return scenario.response;
|
|
7679
|
+
}
|
|
7680
|
+
}
|
|
7681
|
+
}
|
|
7682
|
+
return fixture.default_response;
|
|
7683
|
+
}
|
|
7684
|
+
|
|
7685
|
+
// src/infrastructure/mock/streaming-simulator.ts
|
|
7686
|
+
var DEFAULT_TOKEN_DELAY_MS = 30;
|
|
7687
|
+
async function simulateStreaming(text2, delayMs = DEFAULT_TOKEN_DELAY_MS, onBlock, onComplete) {
|
|
7688
|
+
const tokens = tokenize(text2);
|
|
7689
|
+
let accumulated = "";
|
|
7690
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
7691
|
+
accumulated += tokens[i];
|
|
7692
|
+
const block = {
|
|
7693
|
+
type: "text",
|
|
7694
|
+
text: accumulated
|
|
7695
|
+
};
|
|
7696
|
+
onBlock([block], false);
|
|
7697
|
+
if (i < tokens.length - 1) {
|
|
7698
|
+
await sleep(delayMs);
|
|
7699
|
+
}
|
|
7700
|
+
}
|
|
7701
|
+
const finalBlock = {
|
|
7702
|
+
type: "text",
|
|
7703
|
+
text: accumulated
|
|
7704
|
+
};
|
|
7705
|
+
onBlock([finalBlock], true);
|
|
7706
|
+
onComplete();
|
|
7707
|
+
}
|
|
7708
|
+
function tokenize(text2) {
|
|
7709
|
+
const tokens = [];
|
|
7710
|
+
let current = "";
|
|
7711
|
+
for (const char of text2) {
|
|
7712
|
+
if (char === " ") {
|
|
7713
|
+
if (current) {
|
|
7714
|
+
tokens.push(current);
|
|
7715
|
+
current = "";
|
|
7716
|
+
}
|
|
7717
|
+
tokens.push(" ");
|
|
7718
|
+
} else if (/[.,!?;:\n]/.test(char)) {
|
|
7719
|
+
if (current) {
|
|
7720
|
+
tokens.push(current);
|
|
7721
|
+
current = "";
|
|
7722
|
+
}
|
|
7723
|
+
tokens.push(char);
|
|
7724
|
+
} else {
|
|
7725
|
+
current += char;
|
|
7726
|
+
}
|
|
7727
|
+
}
|
|
7728
|
+
if (current) {
|
|
7729
|
+
tokens.push(current);
|
|
7730
|
+
}
|
|
7731
|
+
return tokens;
|
|
7732
|
+
}
|
|
7733
|
+
function sleep(ms) {
|
|
7734
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
7735
|
+
}
|
|
7736
|
+
|
|
7737
|
+
// src/infrastructure/mock/mock-supervisor-agent.ts
|
|
7738
|
+
var MockSupervisorAgent = class extends EventEmitter5 {
|
|
7739
|
+
logger;
|
|
7740
|
+
fixturesPath;
|
|
7741
|
+
fixture;
|
|
7742
|
+
conversationHistory = [];
|
|
7743
|
+
isExecuting = false;
|
|
7744
|
+
isProcessingCommand = false;
|
|
7745
|
+
isCancelled = false;
|
|
7746
|
+
abortController = null;
|
|
7747
|
+
constructor(config2) {
|
|
7748
|
+
super();
|
|
7749
|
+
this.logger = config2.logger.child({ component: "MockSupervisorAgent" });
|
|
7750
|
+
this.fixturesPath = config2.fixturesPath;
|
|
7751
|
+
this.fixture = loadFixture("supervisor", this.fixturesPath);
|
|
7752
|
+
if (this.fixture) {
|
|
7753
|
+
this.logger.info(
|
|
7754
|
+
{ scenarios: Object.keys(this.fixture.scenarios).length },
|
|
7755
|
+
"Mock Supervisor Agent initialized with fixtures"
|
|
7756
|
+
);
|
|
7757
|
+
} else {
|
|
7758
|
+
this.logger.warn(
|
|
7759
|
+
"Mock Supervisor Agent initialized without fixtures - will return default responses"
|
|
7760
|
+
);
|
|
7761
|
+
}
|
|
7762
|
+
}
|
|
7763
|
+
/**
|
|
7764
|
+
* Executes a command (non-streaming).
|
|
7765
|
+
*/
|
|
7766
|
+
async execute(command, deviceId, _currentSessionId) {
|
|
7767
|
+
this.logger.info({ command, deviceId }, "Mock supervisor execute");
|
|
7768
|
+
const response = this.getResponse(command);
|
|
7769
|
+
this.conversationHistory.push({ role: "user", content: command });
|
|
7770
|
+
this.conversationHistory.push({ role: "assistant", content: response.text });
|
|
7771
|
+
return {
|
|
7772
|
+
output: response.text
|
|
7773
|
+
};
|
|
7774
|
+
}
|
|
7775
|
+
/**
|
|
7776
|
+
* Executes a command with simulated streaming.
|
|
7777
|
+
*/
|
|
7778
|
+
async executeWithStream(command, deviceId) {
|
|
7779
|
+
this.logger.info({ command, deviceId }, "Mock supervisor executeWithStream");
|
|
7780
|
+
this.abortController = new AbortController();
|
|
7781
|
+
this.isExecuting = true;
|
|
7782
|
+
this.isCancelled = false;
|
|
7783
|
+
try {
|
|
7784
|
+
const response = this.getResponse(command);
|
|
7785
|
+
if (this.isExecuting && !this.isCancelled) {
|
|
7786
|
+
const statusBlock = createStatusBlock("Processing...");
|
|
7787
|
+
this.emit("blocks", deviceId, [statusBlock], false);
|
|
7788
|
+
}
|
|
7789
|
+
await this.sleep(100);
|
|
7790
|
+
const allBlocks = [];
|
|
7791
|
+
await simulateStreaming(
|
|
7792
|
+
response.text,
|
|
7793
|
+
response.delay_ms ?? 30,
|
|
7794
|
+
(blocks, isComplete) => {
|
|
7795
|
+
if (this.isExecuting && !this.isCancelled) {
|
|
7796
|
+
this.emit("blocks", deviceId, blocks, false);
|
|
7797
|
+
if (isComplete) {
|
|
7798
|
+
allBlocks.push(...blocks);
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7801
|
+
},
|
|
7802
|
+
() => {
|
|
7803
|
+
}
|
|
7804
|
+
);
|
|
7805
|
+
if (this.isExecuting && !this.isCancelled) {
|
|
7806
|
+
this.conversationHistory.push({ role: "user", content: command });
|
|
7807
|
+
this.conversationHistory.push({
|
|
7808
|
+
role: "assistant",
|
|
7809
|
+
content: response.text
|
|
7810
|
+
});
|
|
7811
|
+
const completionBlock = createStatusBlock("Complete");
|
|
7812
|
+
this.emit(
|
|
7813
|
+
"blocks",
|
|
7814
|
+
deviceId,
|
|
7815
|
+
[completionBlock],
|
|
7816
|
+
true,
|
|
7817
|
+
response.text,
|
|
7818
|
+
allBlocks
|
|
7819
|
+
);
|
|
7820
|
+
}
|
|
7821
|
+
} catch (error) {
|
|
7822
|
+
if (this.isCancelled) {
|
|
7823
|
+
this.logger.info({ deviceId }, "Mock supervisor cancelled");
|
|
7824
|
+
return;
|
|
7825
|
+
}
|
|
7826
|
+
this.logger.error({ error, command }, "Mock supervisor error");
|
|
7827
|
+
const errorBlock = createTextBlock(
|
|
7828
|
+
error instanceof Error ? error.message : "An error occurred"
|
|
7829
|
+
);
|
|
7830
|
+
this.emit("blocks", deviceId, [errorBlock], true);
|
|
7831
|
+
} finally {
|
|
7832
|
+
this.isExecuting = false;
|
|
7833
|
+
this.abortController = null;
|
|
7834
|
+
}
|
|
7835
|
+
}
|
|
7836
|
+
/**
|
|
7837
|
+
* Gets the response for a command from fixtures.
|
|
7838
|
+
*/
|
|
7839
|
+
getResponse(command) {
|
|
7840
|
+
if (!this.fixture) {
|
|
7841
|
+
return {
|
|
7842
|
+
text: "I'm the Supervisor agent. I can help you manage workspaces, sessions, and more. What would you like to do?",
|
|
7843
|
+
delay_ms: 30
|
|
7844
|
+
};
|
|
7845
|
+
}
|
|
7846
|
+
return findMatchingResponse(this.fixture, command);
|
|
7847
|
+
}
|
|
7848
|
+
/**
|
|
7849
|
+
* Cancels current execution.
|
|
7850
|
+
*/
|
|
7851
|
+
cancel() {
|
|
7852
|
+
if (!this.isProcessingCommand && !this.isExecuting) {
|
|
7853
|
+
return false;
|
|
7854
|
+
}
|
|
7855
|
+
this.logger.info("Cancelling mock supervisor execution");
|
|
7856
|
+
this.isCancelled = true;
|
|
7857
|
+
this.isExecuting = false;
|
|
7858
|
+
this.isProcessingCommand = false;
|
|
7859
|
+
if (this.abortController) {
|
|
7860
|
+
this.abortController.abort();
|
|
7861
|
+
}
|
|
7862
|
+
return true;
|
|
7863
|
+
}
|
|
7864
|
+
/**
|
|
7865
|
+
* Check if execution was cancelled.
|
|
7866
|
+
*/
|
|
7867
|
+
wasCancelled() {
|
|
7868
|
+
return this.isCancelled;
|
|
7869
|
+
}
|
|
7870
|
+
/**
|
|
7871
|
+
* Starts command processing.
|
|
7872
|
+
*/
|
|
7873
|
+
startProcessing() {
|
|
7874
|
+
this.abortController = new AbortController();
|
|
7875
|
+
this.isProcessingCommand = true;
|
|
7876
|
+
this.isCancelled = false;
|
|
7877
|
+
return this.abortController;
|
|
7878
|
+
}
|
|
7879
|
+
/**
|
|
7880
|
+
* Checks if processing is active.
|
|
7881
|
+
*/
|
|
7882
|
+
isProcessing() {
|
|
7883
|
+
return this.isProcessingCommand || this.isExecuting;
|
|
7884
|
+
}
|
|
7885
|
+
/**
|
|
7886
|
+
* Ends command processing.
|
|
7887
|
+
*/
|
|
7888
|
+
endProcessing() {
|
|
7889
|
+
this.isProcessingCommand = false;
|
|
7890
|
+
}
|
|
7891
|
+
/**
|
|
7892
|
+
* Clears conversation history.
|
|
7893
|
+
*/
|
|
7894
|
+
clearHistory() {
|
|
7895
|
+
this.conversationHistory = [];
|
|
7896
|
+
this.isCancelled = false;
|
|
7897
|
+
this.logger.info("Mock conversation history cleared");
|
|
7898
|
+
}
|
|
7899
|
+
/**
|
|
7900
|
+
* Resets cancellation state.
|
|
7901
|
+
*/
|
|
7902
|
+
resetCancellationState() {
|
|
7903
|
+
this.isCancelled = false;
|
|
7904
|
+
}
|
|
7905
|
+
/**
|
|
7906
|
+
* Restores conversation history.
|
|
7907
|
+
*/
|
|
7908
|
+
restoreHistory(history) {
|
|
7909
|
+
this.conversationHistory = history.slice(-20);
|
|
7910
|
+
}
|
|
7911
|
+
/**
|
|
7912
|
+
* Gets conversation history.
|
|
7913
|
+
*/
|
|
7914
|
+
getConversationHistory() {
|
|
7915
|
+
return [...this.conversationHistory];
|
|
7916
|
+
}
|
|
7917
|
+
/**
|
|
7918
|
+
* Sleep utility.
|
|
7919
|
+
*/
|
|
7920
|
+
sleep(ms) {
|
|
7921
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
7922
|
+
}
|
|
7923
|
+
};
|
|
7924
|
+
|
|
7925
|
+
// src/infrastructure/mock/mock-agent-session-manager.ts
|
|
7926
|
+
import { EventEmitter as EventEmitter6 } from "events";
|
|
7927
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
7928
|
+
var MockAgentSessionManager = class extends EventEmitter6 {
|
|
7929
|
+
sessions = /* @__PURE__ */ new Map();
|
|
7930
|
+
fixtures = /* @__PURE__ */ new Map();
|
|
7931
|
+
logger;
|
|
7932
|
+
fixturesPath;
|
|
7933
|
+
constructor(config2) {
|
|
7934
|
+
super();
|
|
7935
|
+
this.logger = config2.logger.child({ component: "MockAgentSessionManager" });
|
|
7936
|
+
this.fixturesPath = config2.fixturesPath;
|
|
7937
|
+
this.loadAgentFixtures();
|
|
7938
|
+
this.logger.info("Mock Agent Session Manager initialized");
|
|
7939
|
+
}
|
|
7940
|
+
/**
|
|
7941
|
+
* Pre-load fixtures for all agent types.
|
|
7942
|
+
*/
|
|
7943
|
+
loadAgentFixtures() {
|
|
7944
|
+
const agentTypes = ["cursor", "claude", "opencode"];
|
|
7945
|
+
for (const agentType of agentTypes) {
|
|
7946
|
+
const fixture = loadFixture(agentType, this.fixturesPath);
|
|
7947
|
+
this.fixtures.set(agentType, fixture);
|
|
7948
|
+
if (fixture) {
|
|
7949
|
+
this.logger.debug(
|
|
7950
|
+
{ agentType, scenarios: Object.keys(fixture.scenarios).length },
|
|
7951
|
+
"Loaded fixture for agent type"
|
|
7952
|
+
);
|
|
7953
|
+
}
|
|
7954
|
+
}
|
|
7955
|
+
}
|
|
7956
|
+
/**
|
|
7957
|
+
* Create a new mock agent session.
|
|
7958
|
+
*/
|
|
7959
|
+
createSession(agentType, workingDir, sessionId, agentName) {
|
|
7960
|
+
const id = sessionId ?? `agent-${randomUUID3()}`;
|
|
7961
|
+
const resolvedAgentName = agentName ?? agentType;
|
|
7962
|
+
const state = {
|
|
7963
|
+
sessionId: id,
|
|
7964
|
+
agentType,
|
|
7965
|
+
agentName: resolvedAgentName,
|
|
7966
|
+
workingDir,
|
|
7967
|
+
cliSessionId: `mock-cli-${randomUUID3().slice(0, 8)}`,
|
|
7968
|
+
isExecuting: false,
|
|
7969
|
+
isCancelled: false,
|
|
7970
|
+
messages: [],
|
|
7971
|
+
createdAt: Date.now(),
|
|
7972
|
+
lastActivityAt: Date.now()
|
|
7973
|
+
};
|
|
7974
|
+
this.sessions.set(id, state);
|
|
7975
|
+
this.logger.info(
|
|
7976
|
+
{ sessionId: id, agentType, agentName: resolvedAgentName, workingDir },
|
|
7977
|
+
"Mock agent session created"
|
|
7978
|
+
);
|
|
7979
|
+
this.emit("sessionCreated", state);
|
|
7980
|
+
this.emit("cliSessionIdDiscovered", id, state.cliSessionId);
|
|
7981
|
+
return state;
|
|
7982
|
+
}
|
|
7983
|
+
/**
|
|
7984
|
+
* Execute a command in a mock agent session with simulated streaming.
|
|
7985
|
+
*/
|
|
7986
|
+
async executeCommand(sessionId, prompt) {
|
|
7987
|
+
const state = this.sessions.get(sessionId);
|
|
7988
|
+
if (!state) {
|
|
7989
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
7990
|
+
}
|
|
7991
|
+
if (state.isExecuting) {
|
|
7992
|
+
this.cancelCommand(sessionId);
|
|
7993
|
+
}
|
|
7994
|
+
state.isExecuting = true;
|
|
7995
|
+
state.isCancelled = false;
|
|
7996
|
+
state.lastActivityAt = Date.now();
|
|
7997
|
+
const userMessage = {
|
|
7998
|
+
id: randomUUID3(),
|
|
7999
|
+
timestamp: Date.now(),
|
|
8000
|
+
role: "user",
|
|
8001
|
+
blocks: [createTextBlock(prompt)]
|
|
8002
|
+
};
|
|
8003
|
+
state.messages.push(userMessage);
|
|
8004
|
+
try {
|
|
8005
|
+
const fixture = this.fixtures.get(state.agentType);
|
|
8006
|
+
const response = fixture ? findMatchingResponse(fixture, prompt) : this.getDefaultResponse(state.agentType);
|
|
8007
|
+
await this.sleep(150);
|
|
8008
|
+
const allBlocks = [];
|
|
8009
|
+
await simulateStreaming(
|
|
8010
|
+
response.text,
|
|
8011
|
+
response.delay_ms ?? 30,
|
|
8012
|
+
(blocks, _isComplete) => {
|
|
8013
|
+
if (state.isExecuting && !state.isCancelled) {
|
|
8014
|
+
this.emit("blocks", sessionId, blocks, false);
|
|
8015
|
+
}
|
|
8016
|
+
},
|
|
8017
|
+
() => {
|
|
8018
|
+
}
|
|
8019
|
+
);
|
|
8020
|
+
if (state.isExecuting && !state.isCancelled) {
|
|
8021
|
+
const assistantMessage = {
|
|
8022
|
+
id: randomUUID3(),
|
|
8023
|
+
timestamp: Date.now(),
|
|
8024
|
+
role: "assistant",
|
|
8025
|
+
blocks: [createTextBlock(response.text)]
|
|
8026
|
+
};
|
|
8027
|
+
state.messages.push(assistantMessage);
|
|
8028
|
+
allBlocks.push(createTextBlock(response.text));
|
|
8029
|
+
const completionBlocks = [createStatusBlock("Command completed")];
|
|
8030
|
+
const completionMsg = {
|
|
8031
|
+
id: randomUUID3(),
|
|
8032
|
+
timestamp: Date.now(),
|
|
8033
|
+
role: "system",
|
|
8034
|
+
blocks: completionBlocks
|
|
8035
|
+
};
|
|
8036
|
+
state.messages.push(completionMsg);
|
|
8037
|
+
this.emit("blocks", sessionId, completionBlocks, true);
|
|
8038
|
+
}
|
|
8039
|
+
} catch (error) {
|
|
8040
|
+
if (!state.isCancelled) {
|
|
8041
|
+
this.logger.error({ sessionId, error }, "Mock command execution error");
|
|
8042
|
+
const errorBlocks = [
|
|
8043
|
+
createTextBlock(
|
|
8044
|
+
error instanceof Error ? error.message : "An error occurred"
|
|
8045
|
+
)
|
|
8046
|
+
];
|
|
8047
|
+
this.emit("blocks", sessionId, errorBlocks, true);
|
|
8048
|
+
}
|
|
8049
|
+
} finally {
|
|
8050
|
+
state.isExecuting = false;
|
|
8051
|
+
state.lastActivityAt = Date.now();
|
|
6507
8052
|
}
|
|
6508
|
-
return true;
|
|
6509
8053
|
}
|
|
6510
8054
|
/**
|
|
6511
|
-
*
|
|
6512
|
-
* Used by main.ts to filter out any late-arriving blocks.
|
|
8055
|
+
* Get default response when no fixture is available.
|
|
6513
8056
|
*/
|
|
6514
|
-
|
|
6515
|
-
|
|
8057
|
+
getDefaultResponse(agentType) {
|
|
8058
|
+
const responses = {
|
|
8059
|
+
claude: "I'm Claude, an AI assistant. I can help you with coding tasks, answer questions, and assist with various development workflows. What would you like me to help you with?",
|
|
8060
|
+
cursor: "I'm Cursor AI, ready to help you write and edit code. I can assist with code completion, refactoring, and explaining complex code. What can I help you with today?",
|
|
8061
|
+
opencode: "I'm OpenCode, an open-source AI coding assistant. I can help with code generation, debugging, and documentation. How can I assist you?"
|
|
8062
|
+
};
|
|
8063
|
+
return {
|
|
8064
|
+
text: responses[agentType],
|
|
8065
|
+
delay_ms: 30
|
|
8066
|
+
};
|
|
6516
8067
|
}
|
|
6517
8068
|
/**
|
|
6518
|
-
*
|
|
6519
|
-
* Returns an AbortController that can be used to cancel STT and other operations.
|
|
8069
|
+
* Cancel current command execution.
|
|
6520
8070
|
*/
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
8071
|
+
cancelCommand(sessionId) {
|
|
8072
|
+
const state = this.sessions.get(sessionId);
|
|
8073
|
+
if (!state?.isExecuting) {
|
|
8074
|
+
return;
|
|
8075
|
+
}
|
|
8076
|
+
this.logger.info({ sessionId }, "Cancelling mock command execution");
|
|
8077
|
+
state.isExecuting = false;
|
|
8078
|
+
state.isCancelled = true;
|
|
8079
|
+
state.lastActivityAt = Date.now();
|
|
6527
8080
|
}
|
|
6528
8081
|
/**
|
|
6529
|
-
*
|
|
8082
|
+
* Clear chat history for a session.
|
|
6530
8083
|
*/
|
|
6531
|
-
|
|
6532
|
-
|
|
8084
|
+
clearHistory(sessionId) {
|
|
8085
|
+
const state = this.sessions.get(sessionId);
|
|
8086
|
+
if (!state) {
|
|
8087
|
+
return;
|
|
8088
|
+
}
|
|
8089
|
+
state.messages = [];
|
|
8090
|
+
this.logger.info({ sessionId }, "Mock session history cleared");
|
|
6533
8091
|
}
|
|
6534
8092
|
/**
|
|
6535
|
-
*
|
|
8093
|
+
* Terminate an agent session.
|
|
6536
8094
|
*/
|
|
6537
|
-
|
|
6538
|
-
this.
|
|
6539
|
-
this.logger.
|
|
8095
|
+
terminateSession(sessionId) {
|
|
8096
|
+
this.sessions.delete(sessionId);
|
|
8097
|
+
this.logger.info({ sessionId }, "Mock agent session terminated");
|
|
8098
|
+
this.emit("sessionTerminated", sessionId);
|
|
6540
8099
|
}
|
|
6541
8100
|
/**
|
|
6542
|
-
*
|
|
6543
|
-
* Also resets cancellation state to allow new commands.
|
|
8101
|
+
* Get session state.
|
|
6544
8102
|
*/
|
|
6545
|
-
|
|
6546
|
-
this.
|
|
6547
|
-
this.isCancelled = false;
|
|
6548
|
-
this.logger.info("Global conversation history cleared");
|
|
8103
|
+
getSession(sessionId) {
|
|
8104
|
+
return this.sessions.get(sessionId);
|
|
6549
8105
|
}
|
|
6550
8106
|
/**
|
|
6551
|
-
*
|
|
6552
|
-
* Call this before starting a new command to ensure previous cancellation doesn't affect it.
|
|
8107
|
+
* List all active sessions.
|
|
6553
8108
|
*/
|
|
6554
|
-
|
|
6555
|
-
this.
|
|
8109
|
+
listSessions() {
|
|
8110
|
+
return Array.from(this.sessions.values());
|
|
6556
8111
|
}
|
|
6557
8112
|
/**
|
|
6558
|
-
*
|
|
6559
|
-
* Called on startup to sync in-memory cache with database.
|
|
8113
|
+
* Get chat history for a session.
|
|
6560
8114
|
*/
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
this.conversationHistory = history.slice(-20);
|
|
6564
|
-
this.logger.debug({ messageCount: this.conversationHistory.length }, "Global conversation history restored");
|
|
8115
|
+
getMessages(sessionId) {
|
|
8116
|
+
return this.sessions.get(sessionId)?.messages ?? [];
|
|
6565
8117
|
}
|
|
6566
8118
|
/**
|
|
6567
|
-
*
|
|
8119
|
+
* Check if a session is executing.
|
|
6568
8120
|
*/
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
Your role is to help users:
|
|
6573
|
-
1. **Discover workspaces and projects** - List available workspaces and projects
|
|
6574
|
-
2. **Manage git worktrees** - Create, list, and remove worktrees for parallel development
|
|
6575
|
-
3. **Manage sessions** - Create and terminate agent sessions (Cursor, Claude, OpenCode) and terminal sessions
|
|
6576
|
-
4. **Navigate the file system** - List directories and read files
|
|
6577
|
-
|
|
6578
|
-
## CRITICAL: Always Use Tools - Never Be Lazy
|
|
6579
|
-
|
|
6580
|
-
**YOU MUST ALWAYS call tools to execute user requests. NEVER be lazy or skip actions based on memory or previous context.**
|
|
6581
|
-
|
|
6582
|
-
### Mandatory Tool Usage Rules:
|
|
6583
|
-
|
|
6584
|
-
1. **ALWAYS call tools for fresh data** - When user asks about workspaces, projects, sessions, or any system state:
|
|
6585
|
-
- ALWAYS call the appropriate tool (list_workspaces, list_projects, list_sessions, etc.)
|
|
6586
|
-
- NEVER respond from memory or previous conversation context
|
|
6587
|
-
- System state changes constantly - what was true before may not be true now
|
|
6588
|
-
|
|
6589
|
-
2. **ALWAYS execute requested actions** - When user asks to create, terminate, or modify something:
|
|
6590
|
-
- ALWAYS call the tool to perform the action, even if you think it was done before
|
|
6591
|
-
- If user asks to create a session and one already exists, CREATE ANOTHER ONE (user knows what they want)
|
|
6592
|
-
- If user asks to list projects, LIST THEM NOW with a tool call (don't say "I already showed you")
|
|
6593
|
-
- NEVER refuse a direct request because "it was already done" or "nothing changed"
|
|
6594
|
-
|
|
6595
|
-
3. **User intent is paramount** - When user explicitly requests an action:
|
|
6596
|
-
- Execute it immediately without questioning or suggesting alternatives
|
|
6597
|
-
- Don't assume user made a mistake - they know what they need
|
|
6598
|
-
- Multiple sessions in the same project is a valid use case
|
|
6599
|
-
- Refreshing information is always valid
|
|
6600
|
-
|
|
6601
|
-
4. **No shortcuts** - You must:
|
|
6602
|
-
- Call list_workspaces/list_projects EVERY time user asks what workspaces/projects exist
|
|
6603
|
-
- Call list_sessions EVERY time user asks about active sessions
|
|
6604
|
-
- Call create_agent_session/create_terminal_session EVERY time user asks to create a session
|
|
6605
|
-
- Never say "based on our previous conversation" or "as I mentioned earlier" for factual data
|
|
6606
|
-
|
|
6607
|
-
## Guidelines:
|
|
6608
|
-
- Be concise and helpful
|
|
6609
|
-
- Use tools to gather information before responding
|
|
6610
|
-
- When creating sessions, always confirm the workspace and project first
|
|
6611
|
-
- For ambiguous requests, ask clarifying questions
|
|
6612
|
-
- Format responses for terminal display (avoid markdown links)
|
|
6613
|
-
|
|
6614
|
-
## Session Types:
|
|
6615
|
-
- **cursor** - Cursor AI agent for code assistance
|
|
6616
|
-
- **claude** - Claude Code CLI for AI coding
|
|
6617
|
-
- **opencode** - OpenCode AI agent
|
|
6618
|
-
- **terminal** - Shell terminal for direct commands
|
|
6619
|
-
|
|
6620
|
-
## Creating Agent Sessions:
|
|
6621
|
-
When creating agent sessions, by default use the main project directory (main or master branch) unless the user explicitly requests a specific worktree or branch:
|
|
6622
|
-
- **Default behavior**: Omit the \`worktree\` parameter to create session on the main/master branch (project root directory)
|
|
6623
|
-
- **Specific worktree**: Only specify \`worktree\` when the user explicitly asks for a feature branch worktree (NOT the main branch)
|
|
6624
|
-
- **IMPORTANT**: When \`list_worktrees\` shows a worktree named "main" with \`isMain: true\`, this represents the project root directory. Do NOT pass \`worktree: "main"\` - instead, omit the worktree parameter entirely to use the project root.
|
|
6625
|
-
- **Example**: If user says "start claude on tiflis-code", create session WITHOUT worktree parameter (uses project root on main branch)
|
|
6626
|
-
- **Example**: If user says "start claude on tiflis-code feature/auth branch", list worktrees, find the feature worktree name (e.g., "feature-auth"), and pass that as worktree
|
|
6627
|
-
|
|
6628
|
-
## Worktree Management:
|
|
6629
|
-
Worktrees allow working on multiple branches simultaneously in separate directories.
|
|
6630
|
-
- **Branch naming**: Use conventional format \`<type>/<name>\` where \`<name>\` is lower-kebab-case. Types: \`feature\`, \`fix\`, \`refactor\`, \`docs\`, \`chore\`. Examples: \`feature/user-auth\`, \`fix/keyboard-layout\`, \`refactor/websocket-handler\`
|
|
6631
|
-
- **Directory pattern**: \`project--branch-name\` (slashes replaced with dashes, e.g., \`my-app--feature-user-auth\`)
|
|
6632
|
-
- **Creating worktrees**: Use \`create_worktree\` tool with:
|
|
6633
|
-
- \`createNewBranch: true\` \u2014 Creates a NEW branch and worktree (most common for new features)
|
|
6634
|
-
- \`createNewBranch: false\` \u2014 Checks out an EXISTING branch into a worktree
|
|
6635
|
-
- \`baseBranch\` \u2014 Optional starting point for new branches (defaults to HEAD, commonly "main")
|
|
6636
|
-
- **Example**: To start work on a new feature, create worktree with \`createNewBranch: true\`, \`branch: "feature/new-keyboard"\`, \`baseBranch: "main"\``;
|
|
6637
|
-
return [new HumanMessage(`[System Instructions]
|
|
6638
|
-
${systemPrompt}
|
|
6639
|
-
[End Instructions]`)];
|
|
8121
|
+
isExecuting(sessionId) {
|
|
8122
|
+
return this.sessions.get(sessionId)?.isExecuting ?? false;
|
|
6640
8123
|
}
|
|
6641
8124
|
/**
|
|
6642
|
-
*
|
|
8125
|
+
* Check if a session was cancelled.
|
|
6643
8126
|
*/
|
|
6644
|
-
|
|
6645
|
-
return
|
|
6646
|
-
(entry) => entry.role === "user" ? new HumanMessage(entry.content) : new AIMessage(entry.content)
|
|
6647
|
-
);
|
|
8127
|
+
wasCancelled(sessionId) {
|
|
8128
|
+
return this.sessions.get(sessionId)?.isCancelled ?? false;
|
|
6648
8129
|
}
|
|
6649
8130
|
/**
|
|
6650
|
-
*
|
|
8131
|
+
* Cleanup all sessions.
|
|
6651
8132
|
*/
|
|
6652
|
-
|
|
6653
|
-
|
|
8133
|
+
cleanup() {
|
|
8134
|
+
const sessionIds = Array.from(this.sessions.keys());
|
|
8135
|
+
for (const id of sessionIds) {
|
|
8136
|
+
this.terminateSession(id);
|
|
8137
|
+
}
|
|
6654
8138
|
}
|
|
6655
8139
|
/**
|
|
6656
|
-
*
|
|
8140
|
+
* Sleep utility.
|
|
6657
8141
|
*/
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
if (this.conversationHistory.length > 20) {
|
|
6661
|
-
this.conversationHistory.splice(0, this.conversationHistory.length - 20);
|
|
6662
|
-
}
|
|
8142
|
+
sleep(ms) {
|
|
8143
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
6663
8144
|
}
|
|
6664
8145
|
};
|
|
6665
8146
|
|
|
6666
8147
|
// src/infrastructure/speech/stt-service.ts
|
|
6667
8148
|
import { writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
|
|
6668
|
-
import { join as
|
|
8149
|
+
import { join as join8 } from "path";
|
|
6669
8150
|
import { tmpdir } from "os";
|
|
6670
|
-
import { randomUUID as
|
|
8151
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
6671
8152
|
var STTService = class {
|
|
6672
8153
|
config;
|
|
6673
8154
|
logger;
|
|
@@ -6708,7 +8189,7 @@ var STTService = class {
|
|
|
6708
8189
|
async transcribeOpenAI(audioBuffer, format, signal) {
|
|
6709
8190
|
const baseUrl = this.config.baseUrl ?? "https://api.openai.com/v1";
|
|
6710
8191
|
const endpoint = `${baseUrl}/audio/transcriptions`;
|
|
6711
|
-
const tempPath =
|
|
8192
|
+
const tempPath = join8(tmpdir(), `stt-${randomUUID4()}.${format}`);
|
|
6712
8193
|
try {
|
|
6713
8194
|
if (signal?.aborted) {
|
|
6714
8195
|
throw new Error("Transcription cancelled");
|
|
@@ -6756,7 +8237,7 @@ var STTService = class {
|
|
|
6756
8237
|
async transcribeElevenLabs(audioBuffer, format, signal) {
|
|
6757
8238
|
const baseUrl = this.config.baseUrl ?? "https://api.elevenlabs.io/v1";
|
|
6758
8239
|
const endpoint = `${baseUrl}/speech-to-text`;
|
|
6759
|
-
const tempPath =
|
|
8240
|
+
const tempPath = join8(tmpdir(), `stt-${randomUUID4()}.${format}`);
|
|
6760
8241
|
try {
|
|
6761
8242
|
if (signal?.aborted) {
|
|
6762
8243
|
throw new Error("Transcription cancelled");
|
|
@@ -7269,13 +8750,60 @@ async function bootstrap() {
|
|
|
7269
8750
|
workspacesRoot: env.WORKSPACES_ROOT
|
|
7270
8751
|
});
|
|
7271
8752
|
const ptyManager = new PtyManager({ logger });
|
|
7272
|
-
const agentSessionManager = new AgentSessionManager(logger);
|
|
8753
|
+
const agentSessionManager = env.MOCK_MODE ? new MockAgentSessionManager({ logger, fixturesPath: env.MOCK_FIXTURES_PATH }) : new AgentSessionManager(logger);
|
|
8754
|
+
if (env.MOCK_MODE) {
|
|
8755
|
+
logger.info(
|
|
8756
|
+
{ fixturesPath: env.MOCK_FIXTURES_PATH ?? "built-in" },
|
|
8757
|
+
"Mock mode enabled - using mock agent session manager"
|
|
8758
|
+
);
|
|
8759
|
+
}
|
|
7273
8760
|
const sessionManager = new InMemorySessionManager({
|
|
7274
8761
|
ptyManager,
|
|
7275
8762
|
agentSessionManager,
|
|
7276
8763
|
workspacesRoot: env.WORKSPACES_ROOT,
|
|
7277
8764
|
logger
|
|
7278
8765
|
});
|
|
8766
|
+
if (env.MOCK_MODE) {
|
|
8767
|
+
const mockAgentManager = agentSessionManager;
|
|
8768
|
+
await sessionManager.createSession({
|
|
8769
|
+
sessionType: "supervisor",
|
|
8770
|
+
workingDir: env.WORKSPACES_ROOT
|
|
8771
|
+
});
|
|
8772
|
+
logger.info("Pre-created supervisor session for screenshots");
|
|
8773
|
+
mockAgentManager.createSession(
|
|
8774
|
+
"claude",
|
|
8775
|
+
`${env.WORKSPACES_ROOT}/work/my-app`,
|
|
8776
|
+
"claude-my-app",
|
|
8777
|
+
"claude"
|
|
8778
|
+
);
|
|
8779
|
+
mockAgentManager.createSession(
|
|
8780
|
+
"cursor",
|
|
8781
|
+
`${env.WORKSPACES_ROOT}/personal/blog`,
|
|
8782
|
+
"cursor-blog",
|
|
8783
|
+
"cursor"
|
|
8784
|
+
);
|
|
8785
|
+
mockAgentManager.createSession(
|
|
8786
|
+
"opencode",
|
|
8787
|
+
`${env.WORKSPACES_ROOT}/work/api-service`,
|
|
8788
|
+
"opencode-api",
|
|
8789
|
+
"opencode"
|
|
8790
|
+
);
|
|
8791
|
+
logger.info("Pre-created 3 mock agent sessions for screenshots");
|
|
8792
|
+
chatHistoryService.seedMockData({
|
|
8793
|
+
claude: {
|
|
8794
|
+
id: "claude-my-app",
|
|
8795
|
+
workingDir: `${env.WORKSPACES_ROOT}/work/my-app`
|
|
8796
|
+
},
|
|
8797
|
+
cursor: {
|
|
8798
|
+
id: "cursor-blog",
|
|
8799
|
+
workingDir: `${env.WORKSPACES_ROOT}/personal/blog`
|
|
8800
|
+
},
|
|
8801
|
+
opencode: {
|
|
8802
|
+
id: "opencode-api",
|
|
8803
|
+
workingDir: `${env.WORKSPACES_ROOT}/work/api-service`
|
|
8804
|
+
}
|
|
8805
|
+
});
|
|
8806
|
+
}
|
|
7279
8807
|
const sttService = createSTTService(env, logger);
|
|
7280
8808
|
if (sttService) {
|
|
7281
8809
|
logger.info(
|
|
@@ -7309,7 +8837,10 @@ async function bootstrap() {
|
|
|
7309
8837
|
const cancelledDuringTranscription = /* @__PURE__ */ new Set();
|
|
7310
8838
|
const expectedAuthKey = new AuthKey(env.WORKSTATION_AUTH_KEY);
|
|
7311
8839
|
let messageBroadcaster = null;
|
|
7312
|
-
const supervisorAgent = new
|
|
8840
|
+
const supervisorAgent = env.MOCK_MODE ? new MockSupervisorAgent({
|
|
8841
|
+
logger,
|
|
8842
|
+
fixturesPath: env.MOCK_FIXTURES_PATH
|
|
8843
|
+
}) : new SupervisorAgent({
|
|
7313
8844
|
sessionManager,
|
|
7314
8845
|
agentSessionManager,
|
|
7315
8846
|
workspaceDiscovery,
|
|
@@ -7318,7 +8849,11 @@ async function bootstrap() {
|
|
|
7318
8849
|
getMessageBroadcaster: () => messageBroadcaster,
|
|
7319
8850
|
getChatHistoryService: () => chatHistoryService
|
|
7320
8851
|
});
|
|
7321
|
-
|
|
8852
|
+
if (env.MOCK_MODE) {
|
|
8853
|
+
logger.info("Mock Supervisor Agent initialized for screenshot automation");
|
|
8854
|
+
} else {
|
|
8855
|
+
logger.info("Supervisor Agent initialized with LangGraph");
|
|
8856
|
+
}
|
|
7322
8857
|
const authenticateClient = new AuthenticateClientUseCase({
|
|
7323
8858
|
clientRegistry,
|
|
7324
8859
|
expectedAuthKey,
|
|
@@ -7851,7 +9386,7 @@ async function bootstrap() {
|
|
|
7851
9386
|
logger.info({ wasCancelled }, "supervisorAgent.cancel() returned");
|
|
7852
9387
|
if (messageBroadcaster) {
|
|
7853
9388
|
const cancelBlock = {
|
|
7854
|
-
id:
|
|
9389
|
+
id: randomUUID5(),
|
|
7855
9390
|
block_type: "cancel",
|
|
7856
9391
|
content: "Cancelled by user"
|
|
7857
9392
|
};
|
|
@@ -8086,6 +9621,22 @@ async function bootstrap() {
|
|
|
8086
9621
|
subscribeMessage.device_id,
|
|
8087
9622
|
JSON.stringify(result)
|
|
8088
9623
|
);
|
|
9624
|
+
if (env.MOCK_MODE) {
|
|
9625
|
+
const session = sessionManager.getSession(new SessionId(sessionId));
|
|
9626
|
+
if (session?.type === "terminal") {
|
|
9627
|
+
setTimeout(() => {
|
|
9628
|
+
const clearAndBanner = `clear && printf '\\033[1;36m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\033[0m\\n\\033[1;32m Tiflis Code - Remote Development Workstation\\033[0m\\n\\033[1;36m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\033[0m\\n\\n\\033[1;33m System Information:\\033[0m\\n \u251C\u2500 OS: macOS Sequoia 15.1\\n \u251C\u2500 Shell: zsh 5.9\\n \u251C\u2500 Node: v22.11.0\\n \u2514\u2500 Uptime: 2 days, 14 hours\\n\\n\\033[1;33m Active Sessions:\\033[0m\\n \u251C\u2500 Claude Code \u2500 tiflis/tiflis-code\\n \u251C\u2500 Cursor \u2500 personal/portfolio\\n \u2514\u2500 OpenCode \u2500 tiflis/tiflis-api\\n\\n\\033[1;36m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\033[0m\\n\\n'\r`;
|
|
9629
|
+
ptyManager.write(
|
|
9630
|
+
session,
|
|
9631
|
+
clearAndBanner
|
|
9632
|
+
);
|
|
9633
|
+
logger.info(
|
|
9634
|
+
{ sessionId },
|
|
9635
|
+
"Mock mode: Generated terminal banner on subscribe"
|
|
9636
|
+
);
|
|
9637
|
+
}, 100);
|
|
9638
|
+
}
|
|
9639
|
+
}
|
|
8089
9640
|
}
|
|
8090
9641
|
}
|
|
8091
9642
|
return Promise.resolve();
|
|
@@ -8383,7 +9934,7 @@ async function bootstrap() {
|
|
|
8383
9934
|
}
|
|
8384
9935
|
if (messageBroadcaster) {
|
|
8385
9936
|
const cancelBlock = {
|
|
8386
|
-
id:
|
|
9937
|
+
id: randomUUID5(),
|
|
8387
9938
|
block_type: "cancel",
|
|
8388
9939
|
content: "Cancelled by user"
|
|
8389
9940
|
};
|
|
@@ -9193,6 +10744,40 @@ async function bootstrap() {
|
|
|
9193
10744
|
);
|
|
9194
10745
|
});
|
|
9195
10746
|
});
|
|
10747
|
+
if (env.MOCK_MODE) {
|
|
10748
|
+
const terminalSession = await sessionManager.createSession({
|
|
10749
|
+
sessionType: "terminal",
|
|
10750
|
+
workingDir: env.WORKSPACES_ROOT,
|
|
10751
|
+
terminalSize: { cols: 80, rows: 24 }
|
|
10752
|
+
});
|
|
10753
|
+
logger.info(
|
|
10754
|
+
{ sessionId: terminalSession.id.value },
|
|
10755
|
+
"Pre-created terminal session for screenshots"
|
|
10756
|
+
);
|
|
10757
|
+
if (terminalSession.type === "terminal") {
|
|
10758
|
+
setTimeout(() => {
|
|
10759
|
+
try {
|
|
10760
|
+
const command = `printf $'\\033[1;36m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\033[0m\\n\\033[1;32m Tiflis Code - Remote Development Workstation\\033[0m\\n\\033[1;36m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\033[0m\\n\\n\\033[1;33m System Information:\\033[0m\\n \u251C\u2500 OS: macOS Sequoia 15.1\\n \u251C\u2500 Shell: zsh 5.9\\n \u251C\u2500 Node: v22.11.0\\n \u2514\u2500 Uptime: 2 days, 14 hours\\n\\n\\033[1;33m Active Sessions:\\033[0m\\n \u251C\u2500 Claude Code \u2500 tiflis/tiflis-code\\n \u251C\u2500 Cursor \u2500 personal/portfolio\\n \u2514\u2500 OpenCode \u2500 tiflis/tiflis-api\\n\\n\\033[1;36m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\033[0m\\n\\n'`;
|
|
10761
|
+
ptyManager.write(terminalSession, command + "\r");
|
|
10762
|
+
logger.info("Ran terminal commands for screenshots");
|
|
10763
|
+
setTimeout(() => {
|
|
10764
|
+
const ts = terminalSession;
|
|
10765
|
+
const history = ts.getOutputHistory();
|
|
10766
|
+
logger.info(
|
|
10767
|
+
{
|
|
10768
|
+
sessionId: ts.id.value,
|
|
10769
|
+
bufferSize: history.length,
|
|
10770
|
+
currentSequence: ts.currentSequence
|
|
10771
|
+
},
|
|
10772
|
+
"Terminal output buffer status after command"
|
|
10773
|
+
);
|
|
10774
|
+
}, 500);
|
|
10775
|
+
} catch (error) {
|
|
10776
|
+
logger.warn({ error }, "Failed to run terminal commands");
|
|
10777
|
+
}
|
|
10778
|
+
}, 100);
|
|
10779
|
+
}
|
|
10780
|
+
}
|
|
9196
10781
|
const app = createApp({ env, logger });
|
|
9197
10782
|
registerHealthRoute(
|
|
9198
10783
|
app,
|
|
@@ -9347,6 +10932,14 @@ bootstrap().catch((error) => {
|
|
|
9347
10932
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
9348
10933
|
* @license FSL-1.1-NC
|
|
9349
10934
|
*/
|
|
10935
|
+
/**
|
|
10936
|
+
* @file shell-env.ts
|
|
10937
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
10938
|
+
* @license FSL-1.1-NC
|
|
10939
|
+
*
|
|
10940
|
+
* Utility to get the interactive login shell environment variables,
|
|
10941
|
+
* ensuring PATH and other user-configured variables are properly sourced.
|
|
10942
|
+
*/
|
|
9350
10943
|
/**
|
|
9351
10944
|
* @file pty-manager.ts
|
|
9352
10945
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
@@ -9505,6 +11098,36 @@ bootstrap().catch((error) => {
|
|
|
9505
11098
|
*
|
|
9506
11099
|
* LangGraph-based Supervisor Agent for managing workstation resources.
|
|
9507
11100
|
*/
|
|
11101
|
+
/**
|
|
11102
|
+
* @file fixture-loader.ts
|
|
11103
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
11104
|
+
* @license FSL-1.1-NC
|
|
11105
|
+
*
|
|
11106
|
+
* Loads and parses JSON fixture files for mock mode.
|
|
11107
|
+
*/
|
|
11108
|
+
/**
|
|
11109
|
+
* @file streaming-simulator.ts
|
|
11110
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
11111
|
+
* @license FSL-1.1-NC
|
|
11112
|
+
*
|
|
11113
|
+
* Simulates realistic streaming output for mock responses.
|
|
11114
|
+
*/
|
|
11115
|
+
/**
|
|
11116
|
+
* @file mock-supervisor-agent.ts
|
|
11117
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
11118
|
+
* @license FSL-1.1-NC
|
|
11119
|
+
*
|
|
11120
|
+
* Mock Supervisor Agent for screenshot automation tests.
|
|
11121
|
+
* Returns fixture-based responses with simulated streaming.
|
|
11122
|
+
*/
|
|
11123
|
+
/**
|
|
11124
|
+
* @file mock-agent-session-manager.ts
|
|
11125
|
+
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|
|
11126
|
+
* @license FSL-1.1-NC
|
|
11127
|
+
*
|
|
11128
|
+
* Mock Agent Session Manager for screenshot automation tests.
|
|
11129
|
+
* Simulates agent sessions with fixture-based responses and streaming.
|
|
11130
|
+
*/
|
|
9508
11131
|
/**
|
|
9509
11132
|
* @file stt-service.ts
|
|
9510
11133
|
* @copyright 2025 Roman Barinov <rbarinov@gmail.com>
|