@gajae-code/coding-agent 0.5.4 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/cli/web-search-cli.d.ts +12 -0
  3. package/dist/types/commands/rlm.d.ts +10 -0
  4. package/dist/types/commands/web-search.d.ts +54 -0
  5. package/dist/types/config/keybindings.d.ts +10 -0
  6. package/dist/types/config/model-profiles.d.ts +2 -1
  7. package/dist/types/config/model-registry.d.ts +3 -0
  8. package/dist/types/config/models-config-schema.d.ts +3 -0
  9. package/dist/types/config/settings-schema.d.ts +61 -3
  10. package/dist/types/edit/notebook.d.ts +3 -0
  11. package/dist/types/eval/py/executor.d.ts +3 -0
  12. package/dist/types/eval/py/kernel.d.ts +3 -1
  13. package/dist/types/eval/py/runtime.d.ts +9 -1
  14. package/dist/types/exec/bash-executor.d.ts +4 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  16. package/dist/types/extensibility/custom-tools/wrapper.d.ts +1 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  18. package/dist/types/extensibility/extensions/wrapper.d.ts +1 -0
  19. package/dist/types/gjc-runtime/launch-tmux.d.ts +6 -0
  20. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +14 -0
  21. package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
  22. package/dist/types/gjc-runtime/tmux-gc.d.ts +3 -3
  23. package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -0
  24. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +18 -0
  25. package/dist/types/goals/state.d.ts +1 -1
  26. package/dist/types/goals/tools/goal-tool.d.ts +2 -0
  27. package/dist/types/main.d.ts +11 -0
  28. package/dist/types/modes/components/custom-editor.d.ts +4 -2
  29. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +12 -0
  30. package/dist/types/modes/components/model-selector.d.ts +5 -2
  31. package/dist/types/modes/components/status-line.d.ts +4 -1
  32. package/dist/types/modes/controllers/input-controller.d.ts +3 -0
  33. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  34. package/dist/types/modes/print-mode.d.ts +6 -0
  35. package/dist/types/modes/rpc/rpc-client.d.ts +21 -0
  36. package/dist/types/modes/rpc/rpc-socket-security.d.ts +7 -0
  37. package/dist/types/modes/rpc/rpc-types.d.ts +13 -0
  38. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +2 -0
  39. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +1 -0
  40. package/dist/types/rlm/artifacts.d.ts +9 -0
  41. package/dist/types/rlm/complete-research-tool.d.ts +35 -0
  42. package/dist/types/rlm/data-context.d.ts +6 -0
  43. package/dist/types/rlm/index.d.ts +35 -0
  44. package/dist/types/rlm/notebook.d.ts +12 -0
  45. package/dist/types/rlm/preset.d.ts +23 -0
  46. package/dist/types/rlm/python-tool.d.ts +16 -0
  47. package/dist/types/rlm/report.d.ts +14 -0
  48. package/dist/types/rlm/types.d.ts +37 -0
  49. package/dist/types/sdk.d.ts +7 -0
  50. package/dist/types/session/agent-session.d.ts +21 -0
  51. package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -1
  52. package/dist/types/tools/browser/attach.d.ts +19 -3
  53. package/dist/types/tools/browser/registry.d.ts +15 -0
  54. package/dist/types/tools/browser/render.d.ts +3 -0
  55. package/dist/types/tools/browser.d.ts +18 -1
  56. package/dist/types/tools/computer/render.d.ts +17 -0
  57. package/dist/types/tools/computer.d.ts +465 -0
  58. package/dist/types/tools/index.d.ts +24 -1
  59. package/dist/types/tools/job.d.ts +13 -0
  60. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  61. package/dist/types/web/search/index.d.ts +32 -2
  62. package/dist/types/web/search/providers/base.d.ts +22 -0
  63. package/dist/types/web/search/providers/xai.d.ts +64 -0
  64. package/dist/types/web/search/types.d.ts +11 -3
  65. package/package.json +7 -7
  66. package/src/cli/web-search-cli.ts +123 -8
  67. package/src/cli.ts +2 -0
  68. package/src/commands/rlm.ts +19 -0
  69. package/src/commands/web-search.ts +66 -0
  70. package/src/config/keybindings.ts +11 -0
  71. package/src/config/model-profiles.ts +11 -3
  72. package/src/config/model-registry.ts +55 -1
  73. package/src/config/models-config-schema.ts +1 -0
  74. package/src/config/settings-schema.ts +67 -1
  75. package/src/edit/notebook.ts +6 -2
  76. package/src/eval/py/executor.ts +8 -1
  77. package/src/eval/py/kernel.ts +9 -4
  78. package/src/eval/py/runtime.ts +153 -32
  79. package/src/exec/bash-executor.ts +10 -4
  80. package/src/extensibility/custom-tools/types.ts +2 -0
  81. package/src/extensibility/custom-tools/wrapper.ts +2 -0
  82. package/src/extensibility/extensions/types.ts +2 -0
  83. package/src/extensibility/extensions/wrapper.ts +1 -0
  84. package/src/gjc-runtime/launch-tmux.ts +129 -1
  85. package/src/gjc-runtime/session-state-sidecar.ts +61 -1
  86. package/src/gjc-runtime/tmux-common.ts +26 -2
  87. package/src/gjc-runtime/tmux-gc.ts +40 -27
  88. package/src/gjc-runtime/tmux-sessions.ts +13 -1
  89. package/src/gjc-runtime/ultragoal-runtime.ts +340 -18
  90. package/src/goals/runtime.ts +4 -3
  91. package/src/goals/state.ts +1 -1
  92. package/src/goals/tools/goal-tool.ts +16 -3
  93. package/src/internal-urls/docs-index.generated.ts +13 -9
  94. package/src/main.ts +28 -3
  95. package/src/modes/components/custom-editor.ts +13 -4
  96. package/src/modes/components/custom-model-preset-wizard.ts +293 -0
  97. package/src/modes/components/hook-selector.ts +1 -1
  98. package/src/modes/components/model-selector.ts +72 -29
  99. package/src/modes/components/skill-message.ts +62 -8
  100. package/src/modes/components/status-line.ts +13 -1
  101. package/src/modes/controllers/input-controller.ts +60 -11
  102. package/src/modes/controllers/selector-controller.ts +39 -0
  103. package/src/modes/interactive-mode.ts +1 -1
  104. package/src/modes/print-mode.ts +14 -4
  105. package/src/modes/rpc/rpc-client.ts +250 -80
  106. package/src/modes/rpc/rpc-mode.ts +6 -12
  107. package/src/modes/rpc/rpc-socket-security.ts +103 -0
  108. package/src/modes/rpc/rpc-types.ts +10 -0
  109. package/src/modes/shared/agent-wire/command-dispatch.ts +7 -0
  110. package/src/modes/shared/agent-wire/command-validation.ts +1 -0
  111. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  112. package/src/modes/shared/agent-wire/unattended-session.ts +9 -0
  113. package/src/modes/utils/hotkeys-markdown.ts +4 -2
  114. package/src/modes/utils/ui-helpers.ts +2 -2
  115. package/src/prompts/goals/goal-continuation.md +1 -0
  116. package/src/prompts/goals/goal-mode-active.md +1 -0
  117. package/src/prompts/system/rlm-report-command.md +1 -0
  118. package/src/prompts/system/rlm-research.md +23 -0
  119. package/src/prompts/tools/bash.md +23 -2
  120. package/src/prompts/tools/browser.md +7 -3
  121. package/src/prompts/tools/computer.md +74 -0
  122. package/src/prompts/tools/goal.md +3 -0
  123. package/src/prompts/tools/job.md +9 -1
  124. package/src/prompts/tools/web-search.md +7 -0
  125. package/src/rlm/artifacts.ts +60 -0
  126. package/src/rlm/complete-research-tool.ts +163 -0
  127. package/src/rlm/data-context.ts +26 -0
  128. package/src/rlm/index.ts +339 -0
  129. package/src/rlm/notebook.ts +108 -0
  130. package/src/rlm/preset.ts +76 -0
  131. package/src/rlm/python-tool.ts +68 -0
  132. package/src/rlm/report.ts +70 -0
  133. package/src/rlm/types.ts +40 -0
  134. package/src/sdk.ts +12 -0
  135. package/src/session/agent-session.ts +48 -3
  136. package/src/slash-commands/builtin-registry.ts +17 -0
  137. package/src/tools/bash-allowed-prefixes.ts +84 -1
  138. package/src/tools/bash.ts +80 -13
  139. package/src/tools/browser/attach.ts +103 -3
  140. package/src/tools/browser/registry.ts +176 -2
  141. package/src/tools/browser/render.ts +9 -1
  142. package/src/tools/browser.ts +33 -0
  143. package/src/tools/computer/render.ts +78 -0
  144. package/src/tools/computer.ts +640 -0
  145. package/src/tools/index.ts +41 -1
  146. package/src/tools/job.ts +88 -5
  147. package/src/tools/json-tree.ts +42 -29
  148. package/src/tools/renderers.ts +2 -0
  149. package/src/tools/tool-timeouts.ts +1 -0
  150. package/src/web/search/index.ts +27 -2
  151. package/src/web/search/provider.ts +16 -1
  152. package/src/web/search/providers/base.ts +22 -0
  153. package/src/web/search/providers/xai.ts +511 -0
  154. package/src/web/search/render.ts +7 -0
  155. package/src/web/search/types.ts +11 -1
@@ -3,12 +3,28 @@ import { logger } from "@gajae-code/utils";
3
3
  import type { Subprocess } from "bun";
4
4
  import type { Browser, CDPSession } from "puppeteer-core";
5
5
  import { ToolAbortError, ToolError } from "../tool-errors";
6
- import { findFreeCdpPort, findReusableCdp, gracefulKillTreeOnce, killExistingByPath, waitForCdp } from "./attach";
6
+ import {
7
+ findFreeCdpPort,
8
+ findReusableCdp,
9
+ findRunningChromeProfile,
10
+ gracefulKillTreeOnce,
11
+ killExistingByPath,
12
+ waitForCdp,
13
+ } from "./attach";
7
14
  import { BROWSER_PROTOCOL_TIMEOUT_MS, launchHeadlessBrowser, loadPuppeteer, type UserAgentOverride } from "./launch";
8
15
 
9
16
  export type BrowserKind =
10
17
  | { kind: "headless"; headless: boolean }
11
18
  | { kind: "spawned"; path: string }
19
+ | {
20
+ kind: "chrome-profile";
21
+ path: string;
22
+ userDataDir: string;
23
+ profileDirectory: string;
24
+ background: boolean;
25
+ noFocus: boolean;
26
+ cdpPort?: number;
27
+ }
12
28
  | { kind: "connected"; cdpUrl: string };
13
29
 
14
30
  export type BrowserKindTag = BrowserKind["kind"];
@@ -24,6 +40,8 @@ export interface BrowserHandle {
24
40
  stealth: { browserSession: CDPSession | null; override: UserAgentOverride | null };
25
41
  }
26
42
 
43
+ type SpawnedChromeProfileKind = Extract<BrowserKind, { kind: "chrome-profile" }>;
44
+
27
45
  const browsers = new Map<string, BrowserHandle>();
28
46
 
29
47
  /**
@@ -39,6 +57,8 @@ function browserKey(kind: BrowserKind): string {
39
57
  return `headless:${kind.headless ? "1" : "0"}`;
40
58
  case "spawned":
41
59
  return `spawned:${kind.path}`;
60
+ case "chrome-profile":
61
+ return `chrome-profile:${kind.path}:${kind.userDataDir}:${kind.profileDirectory}:${kind.cdpPort ?? 0}`;
42
62
  case "connected":
43
63
  return `connected:${kind.cdpUrl}`;
44
64
  }
@@ -94,7 +114,160 @@ async function openBrowserHandle(kind: BrowserKind, opts: AcquireBrowserOptions)
94
114
  stealth: { browserSession: null, override: null },
95
115
  };
96
116
  }
117
+ if (kind.kind === "chrome-profile") {
118
+ return await openChromeProfileHandle(kind, opts);
119
+ }
120
+
121
+ return await openSpawnedBrowserHandle(kind, opts);
122
+ }
123
+
124
+ const CHROME_PROFILE_LOCK_FILES = ["SingletonLock", "SingletonSocket", "SingletonCookie"] as const;
125
+
126
+ async function hasChromeProfileLock(userDataDir: string): Promise<boolean> {
127
+ for (const lockFile of CHROME_PROFILE_LOCK_FILES) {
128
+ if (await Bun.file(path.join(userDataDir, lockFile)).exists()) return true;
129
+ }
130
+ return false;
131
+ }
132
+
133
+ const CHROME_PROFILE_MANAGED_FLAGS = new Set([
134
+ "--user-data-dir",
135
+ "--profile-directory",
136
+ "--remote-debugging-address",
137
+ "--remote-debugging-port",
138
+ ]);
139
+
140
+ function filterChromeProfileAppArgs(appArgs: readonly string[] | undefined): string[] {
141
+ if (!appArgs?.length) return [];
142
+ const filtered: string[] = [];
143
+ for (let i = 0; i < appArgs.length; i++) {
144
+ const arg = appArgs[i]!;
145
+ const flagName = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
146
+ if (CHROME_PROFILE_MANAGED_FLAGS.has(flagName)) {
147
+ if (!arg.includes("=") && i < appArgs.length - 1) i++;
148
+ continue;
149
+ }
150
+ filtered.push(arg);
151
+ }
152
+ return filtered;
153
+ }
154
+
155
+ export function buildChromeProfileLaunchArgs(
156
+ kind: SpawnedChromeProfileKind,
157
+ appArgs: readonly string[] | undefined,
158
+ port: number,
159
+ ): string[] {
160
+ const args = [
161
+ ...filterChromeProfileAppArgs(appArgs),
162
+ `--user-data-dir=${kind.userDataDir}`,
163
+ `--profile-directory=${kind.profileDirectory}`,
164
+ `--remote-debugging-port=${port}`,
165
+ "--remote-debugging-address=127.0.0.1",
166
+ ];
167
+ if (kind.background || kind.noFocus) args.push("--no-startup-window");
168
+ return args;
169
+ }
170
+
171
+ export function buildChromeProfileLaunchArgsForTest(
172
+ kind: SpawnedChromeProfileKind,
173
+ appArgs: readonly string[] | undefined,
174
+ port: number,
175
+ ): string[] {
176
+ return buildChromeProfileLaunchArgs(kind, appArgs, port);
177
+ }
178
+
179
+ export async function openChromeProfileHandle(
180
+ kind: SpawnedChromeProfileKind,
181
+ opts: AcquireBrowserOptions,
182
+ ): Promise<BrowserHandle> {
183
+ const exe = kind.path;
184
+ if (!path.isAbsolute(exe)) {
185
+ throw new ToolError(
186
+ `app.path must be absolute for app.browser="chrome" (got ${JSON.stringify(exe)}). Pass the Chrome binary path, not the .app bundle.`,
187
+ );
188
+ }
189
+
190
+ const running = await findRunningChromeProfile(
191
+ exe,
192
+ { userDataDir: kind.userDataDir, profileDirectory: kind.profileDirectory },
193
+ opts.signal,
194
+ );
195
+ let cdpUrl: string;
196
+ let pid: number | undefined;
197
+ let subprocess: Subprocess | undefined;
198
+ if (running?.cdpUrl) {
199
+ logger.debug("Reusing existing Chrome profile CDP endpoint", {
200
+ exe,
201
+ pid: running.pid,
202
+ cdpUrl: running.cdpUrl,
203
+ profileDirectory: kind.profileDirectory,
204
+ });
205
+ cdpUrl = running.cdpUrl;
206
+ pid = running.pid;
207
+ } else if (running) {
208
+ throw new ToolError(
209
+ running.unsafeCdpReason ??
210
+ `Chrome profile ${JSON.stringify(kind.profileDirectory)} under ${kind.userDataDir} is already running without an attachable localhost CDP endpoint. ` +
211
+ "GJC will not kill or relaunch an existing Chrome profile. Close that Chrome profile first, or restart Chrome yourself with --remote-debugging-address=127.0.0.1 and --remote-debugging-port=<port> then use app.cdp_url.",
212
+ );
213
+ } else {
214
+ if (await hasChromeProfileLock(kind.userDataDir)) {
215
+ throw new ToolError(
216
+ `Chrome user data directory ${kind.userDataDir} appears to be locked by an existing Chrome process without an attachable localhost CDP endpoint. ` +
217
+ "GJC will not kill or relaunch an existing Chrome profile. Close that Chrome profile first, or restart Chrome yourself with --remote-debugging-address=127.0.0.1 and --remote-debugging-port=<port> then use app.cdp_url.",
218
+ );
219
+ }
220
+ const port = kind.cdpPort ?? (await findFreeCdpPort());
221
+ const launchArgs = buildChromeProfileLaunchArgs(kind, opts.appArgs, port);
222
+ const child = Bun.spawn([exe, ...launchArgs], {
223
+ stdout: "ignore",
224
+ stderr: "ignore",
225
+ stdin: "ignore",
226
+ });
227
+ child.unref();
228
+ subprocess = child;
229
+ pid = child.pid;
230
+ cdpUrl = `http://127.0.0.1:${port}`;
231
+ try {
232
+ await waitForCdp(cdpUrl, 30_000, opts.signal);
233
+ } catch (err) {
234
+ await gracefulKillTreeOnce(child.pid).catch(() => undefined);
235
+ if (err instanceof ToolAbortError) throw err;
236
+ if (err instanceof Error && err.name === "AbortError") throw err;
237
+ throw new ToolError(
238
+ `Failed to attach to Chrome profile ${JSON.stringify(kind.profileDirectory)} on ${cdpUrl}: ${(err as Error).message}`,
239
+ );
240
+ }
241
+ }
242
+
243
+ const puppeteer = await loadPuppeteer();
244
+ let browser: Browser;
245
+ try {
246
+ browser = await puppeteer.connect({
247
+ browserURL: cdpUrl,
248
+ defaultViewport: null,
249
+ protocolTimeout: BROWSER_PROTOCOL_TIMEOUT_MS,
250
+ });
251
+ } catch (err) {
252
+ if (subprocess) await gracefulKillTreeOnce(subprocess.pid);
253
+ throw new ToolError(`Connected to ${cdpUrl} but puppeteer.connect failed: ${(err as Error).message}`);
254
+ }
255
+ return {
256
+ key: browserKey(kind),
257
+ kind,
258
+ browser,
259
+ cdpUrl,
260
+ pid,
261
+ subprocess,
262
+ refCount: 0,
263
+ stealth: { browserSession: null, override: null },
264
+ };
265
+ }
97
266
 
267
+ async function openSpawnedBrowserHandle(
268
+ kind: Extract<BrowserKind, { kind: "spawned" }>,
269
+ opts: AcquireBrowserOptions,
270
+ ): Promise<BrowserHandle> {
98
271
  const exe = kind.path;
99
272
  if (!path.isAbsolute(exe)) {
100
273
  throw new ToolError(
@@ -203,8 +376,9 @@ async function disposeBrowserHandle(handle: BrowserHandle, opts: { kill: boolean
203
376
  try {
204
377
  handle.browser.disconnect();
205
378
  } catch (err) {
206
- logger.debug("Failed to disconnect from spawned browser", { error: (err as Error).message });
379
+ logger.debug(`Failed to disconnect from ${handle.kind.kind} browser`, { error: (err as Error).message });
207
380
  }
208
381
  }
382
+ if (handle.kind.kind === "chrome-profile" && !handle.subprocess) return;
209
383
  if (opts.kill && handle.pid !== undefined) await gracefulKillTreeOnce(handle.pid);
210
384
  }
@@ -23,7 +23,14 @@ interface BrowserRenderArgs {
23
23
  code?: string;
24
24
  all?: boolean;
25
25
  kill?: boolean;
26
- app?: { path?: string; cdp_url?: string; target?: string };
26
+ app?: {
27
+ path?: string;
28
+ cdp_url?: string;
29
+ browser?: string;
30
+ user_data_dir?: string;
31
+ profile_directory?: string;
32
+ target?: string;
33
+ };
27
34
  viewport?: { width: number; height: number; scale?: number };
28
35
  timeout?: number;
29
36
  }
@@ -35,6 +42,7 @@ interface BrowserRenderContext {
35
42
 
36
43
  function describeBrowser(args: BrowserRenderArgs, details: BrowserToolDetails | undefined): string | undefined {
37
44
  if (args.app?.cdp_url) return `connected ${args.app.cdp_url}`;
45
+ if (args.app?.browser === "chrome") return `Chrome profile ${args.app.profile_directory ?? "<profile>"}`;
38
46
  if (args.app?.path) return `spawned ${shortenPath(args.app.path)}`;
39
47
  switch (details?.browser) {
40
48
  case "headless":
@@ -21,6 +21,12 @@ const DEFAULT_TAB_NAME = "main";
21
21
  const appSchema = z.object({
22
22
  path: z.string().describe("binary path to spawn").optional(),
23
23
  cdp_url: z.string().describe("existing cdp endpoint").optional(),
24
+ browser: z.enum(["chrome"]).describe("existing browser profile mode").optional(),
25
+ user_data_dir: z.string().describe("Chrome user data directory containing profiles").optional(),
26
+ profile_directory: z.string().describe("Chrome profile directory name, e.g. Profile 10").optional(),
27
+ background: z.boolean().describe("prefer background/hidden Chrome profile launch when supported").optional(),
28
+ no_focus: z.boolean().describe("avoid focusing Chrome during profile launch when supported").optional(),
29
+ cdp_port: z.number().int().positive().describe("local CDP port for launched Chrome profile").optional(),
24
30
  args: z.array(z.string()).describe("extra cli args").optional(),
25
31
  target: z.string().describe("substring to pick a window").optional(),
26
32
  });
@@ -104,11 +110,31 @@ export interface BrowserToolDetails {
104
110
  meta?: OutputMeta;
105
111
  }
106
112
 
113
+ export function resolveBrowserKindForTest(params: BrowserParams, session: ToolSession): BrowserKind {
114
+ return resolveBrowserKind(params, session);
115
+ }
116
+
107
117
  function resolveBrowserKind(params: BrowserParams, session: ToolSession): BrowserKind {
108
118
  const app = params.app;
109
119
  if (app?.cdp_url) {
110
120
  return { kind: "connected", cdpUrl: app.cdp_url.replace(/\/+$/, "") };
111
121
  }
122
+ if (app?.browser === "chrome") {
123
+ if (!app.path) throw new ToolError('app.path is required when app.browser is "chrome".');
124
+ if (!app.user_data_dir) throw new ToolError('app.user_data_dir is required when app.browser is "chrome".');
125
+ if (!app.profile_directory)
126
+ throw new ToolError('app.profile_directory is required when app.browser is "chrome".');
127
+ const exe = resolveToCwd(app.path, session.cwd);
128
+ return {
129
+ kind: "chrome-profile",
130
+ path: exe,
131
+ userDataDir: resolveToCwd(app.user_data_dir, session.cwd),
132
+ profileDirectory: app.profile_directory,
133
+ background: app.background ?? false,
134
+ noFocus: app.no_focus ?? false,
135
+ cdpPort: app.cdp_port,
136
+ };
137
+ }
112
138
  if (app?.path) {
113
139
  const exe = resolveToCwd(app.path, session.cwd);
114
140
  return { kind: "spawned", path: exe };
@@ -358,6 +384,8 @@ function describeBrowser(handle: BrowserHandle): string {
358
384
  return `headless browser (${handle.kind.headless ? "hidden" : "visible"})`;
359
385
  case "spawned":
360
386
  return `spawned ${handle.kind.path} (pid ${handle.pid ?? "?"})`;
387
+ case "chrome-profile":
388
+ return `Chrome profile ${handle.kind.profileDirectory} at ${handle.kind.userDataDir} (${handle.subprocess ? `pid ${handle.pid ?? "?"}` : "external CDP"})`;
361
389
  case "connected":
362
390
  return `connected ${handle.cdpUrl ?? handle.kind.cdpUrl}`;
363
391
  }
@@ -369,6 +397,8 @@ function describeKind(kind: BrowserKind): string {
369
397
  return `headless ${kind.headless ? "hidden" : "visible"}`;
370
398
  case "spawned":
371
399
  return `spawned:${kind.path}`;
400
+ case "chrome-profile":
401
+ return `chrome-profile:${kind.path}:${kind.userDataDir}:${kind.profileDirectory}`;
372
402
  case "connected":
373
403
  return `connected:${kind.cdpUrl}`;
374
404
  }
@@ -378,6 +408,9 @@ function sameBrowserKind(a: BrowserKind, b: BrowserKind): boolean {
378
408
  if (a.kind !== b.kind) return false;
379
409
  if (a.kind === "headless" && b.kind === "headless") return a.headless === b.headless;
380
410
  if (a.kind === "spawned" && b.kind === "spawned") return a.path === b.path;
411
+ if (a.kind === "chrome-profile" && b.kind === "chrome-profile") {
412
+ return a.path === b.path && a.userDataDir === b.userDataDir && a.profileDirectory === b.profileDirectory;
413
+ }
381
414
  if (a.kind === "connected" && b.kind === "connected") return a.cdpUrl === b.cdpUrl;
382
415
  return false;
383
416
  }
@@ -0,0 +1,78 @@
1
+ import type { Component } from "@gajae-code/tui";
2
+ import { Text } from "@gajae-code/tui";
3
+ import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
4
+ import type { Theme } from "../../modes/theme/theme";
5
+ import type { ComputerToolDetails } from "../computer";
6
+ import { formatBadge, formatErrorMessage } from "../render-utils";
7
+
8
+ function asRecord(value: unknown): Record<string, unknown> {
9
+ return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
10
+ }
11
+
12
+ function summarizeArgs(args: unknown): string {
13
+ const input = asRecord(args);
14
+ if (input.action === "batch" && Array.isArray(input.actions)) {
15
+ return `batch ${input.actions.length} step${input.actions.length === 1 ? "" : "s"}`;
16
+ }
17
+ const action = typeof input.action === "string" ? input.action : "computer";
18
+ const parts = [action];
19
+ if (typeof input.x === "number" && typeof input.y === "number") parts.push(`@ ${input.x},${input.y}`);
20
+ if (typeof input.to_x === "number" && typeof input.to_y === "number") parts.push(`→ ${input.to_x},${input.to_y}`);
21
+ if (typeof input.scroll_x === "number" || typeof input.scroll_y === "number") {
22
+ parts.push(`scroll ${input.scroll_x ?? 0},${input.scroll_y ?? 0}`);
23
+ }
24
+ if (Array.isArray(input.keys)) parts.push(`keys ${input.keys.join("+")}`);
25
+ if (typeof input.ms === "number") parts.push(`${input.ms}ms`);
26
+ return parts.join(" ");
27
+ }
28
+
29
+ export function summarizeComputerDetails(
30
+ details: ComputerToolDetails | undefined,
31
+ isError: boolean,
32
+ theme: Theme,
33
+ ): string {
34
+ if (!details) return isError ? "Computer action failed" : "Computer action completed";
35
+ if (details.action === "batch" && details.steps) {
36
+ const successCount = details.steps.filter(s => s.status === "success").length;
37
+ const parts: string[] = [`batch ${successCount}/${details.steps.length}`];
38
+ if (details.screenshot) parts.push(`screenshot ${details.screenshot.widthPx}x${details.screenshot.heightPx}`);
39
+ if (details.code) parts.push(theme.fg(isError ? "error" : "muted", details.code));
40
+ return parts.join(" ");
41
+ }
42
+ const parts: string[] = [details.action];
43
+ if (details.x !== undefined && details.y !== undefined) parts.push(`@ ${details.x},${details.y}`);
44
+ if (details.toX !== undefined && details.toY !== undefined) parts.push(`→ ${details.toX},${details.toY}`);
45
+ if (details.scrollX !== undefined || details.scrollY !== undefined)
46
+ parts.push(`scroll ${details.scrollX ?? 0},${details.scrollY ?? 0}`);
47
+ if (details.screenshot) {
48
+ const shot = details.screenshot;
49
+ parts.push(`screenshot ${shot.widthPx}x${shot.heightPx}`);
50
+ if (shot.pngBytes !== undefined) parts.push(`${shot.pngBytes} bytes`);
51
+ if (shot.captureId) parts.push(`capture ${shot.captureId}`);
52
+ }
53
+ if (details.supervisor) parts.push(`supervisor ${details.supervisor}`);
54
+ if (details.code) parts.push(theme.fg(isError ? "error" : "muted", details.code));
55
+ return parts.join(" ");
56
+ }
57
+
58
+ export const computerToolRenderer = {
59
+ renderCall(args: unknown, _options: RenderResultOptions, theme: Theme): Component {
60
+ return new Text(`${formatBadge("computer", "accent", theme)} ${summarizeArgs(args)}`);
61
+ },
62
+ renderResult(
63
+ result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
64
+ _options: RenderResultOptions,
65
+ theme: Theme,
66
+ ): Component {
67
+ if (result.isError) {
68
+ const details = result.details as ComputerToolDetails | undefined;
69
+ return new Text(
70
+ formatErrorMessage(details?.message ?? result.content.find(c => c.type === "text")?.text, theme),
71
+ );
72
+ }
73
+ return new Text(
74
+ `${formatBadge("computer", "success", theme)} ${summarizeComputerDetails(result.details as ComputerToolDetails | undefined, false, theme)}`,
75
+ );
76
+ },
77
+ mergeCallAndResult: true,
78
+ };