@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
@@ -33,9 +33,11 @@ import { AskTool } from "./ask";
33
33
  import { AstEditTool } from "./ast-edit";
34
34
  import { AstGrepTool } from "./ast-grep";
35
35
  import { BashTool } from "./bash";
36
+ import type { BashRestrictionProfile } from "./bash-allowed-prefixes";
36
37
  import { BrowserTool } from "./browser";
37
38
  import { CalculatorTool } from "./calculator";
38
39
  import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
40
+ import { ComputerTool, isComputerCallable, isComputerLoadablePlatform } from "./computer";
39
41
  import { CronCreateTool, CronDeleteTool, CronListTool } from "./cron";
40
42
  import { DebugTool } from "./debug";
41
43
  import { EvalTool } from "./eval";
@@ -73,6 +75,7 @@ export * from "./bash";
73
75
  export * from "./browser";
74
76
  export * from "./calculator";
75
77
  export * from "./checkpoint";
78
+ export * from "./computer";
76
79
  export * from "./cron";
77
80
  export * from "./debug";
78
81
  export * from "./eval";
@@ -159,6 +162,12 @@ export interface ToolSession {
159
162
  assertEvalExecutionAllowed?: () => void;
160
163
  /** Track tool-owned eval work so session disposal can await/abort it like direct session eval runs. */
161
164
  trackEvalExecution?<T>(execution: Promise<T>, abortController: AbortController): Promise<T>;
165
+ /** Register a safe request handler that asks a managed foreground bash call to fold into a background job. */
166
+ registerForegroundBashBackgroundRequestHandler?: (handler: () => void) => () => void;
167
+ /** Whether a managed foreground bash call is currently foldable into a background job. */
168
+ hasForegroundBashBackgroundRequestHandler?: () => boolean;
169
+ /** Request that the active managed foreground bash call fold into a background job, if supported. */
170
+ requestForegroundBashBackground?: () => boolean;
162
171
  /** Get session ID */
163
172
  getSessionId?: () => string | null;
164
173
  /** Get Hindsight runtime state for this agent session. */
@@ -171,8 +180,12 @@ export interface ToolSession {
171
180
  getToolByName?: (name: string) => AgentTool | undefined;
172
181
  /** Agent registry for IRC routing across live sessions. */
173
182
  agentRegistry?: AgentRegistry;
174
- /** Optional restricted bash command prefixes for read-only role agents. */
183
+ /** Optional restricted bash command prefixes for read-only role agents and constrained modes. */
175
184
  bashAllowedPrefixes?: string[];
185
+ /** Restriction policy for sessions that deliberately expose a narrow bash surface. */
186
+ bashRestrictionProfile?: BashRestrictionProfile;
187
+ /** Optional per-session allowlist for tools exposed through search_tool_bm25. */
188
+ discoverableToolAllowedNames?: readonly string[];
176
189
  /** Get artifacts directory for artifact:// URLs */
177
190
  getArtifactsDir?: () => string | null;
178
191
  /** Get the ArtifactManager backing this session (shared across parent + subagents). */
@@ -201,6 +214,8 @@ export interface ToolSession {
201
214
  getGoalModeState?: () => GoalModeState | undefined;
202
215
  /** Unattended workflow-gate emitter (present only when unattended mode is negotiated). */
203
216
  getWorkflowGateEmitter?: () => WorkflowGateEmitter | undefined;
217
+ /** Optional per-session restriction for goal tool operations. */
218
+ goalToolAllowedOps?: readonly ("create" | "get" | "complete" | "resume" | "drop" | "pause")[];
204
219
  /** Goal runtime for the active agent session. */
205
220
  getGoalRuntime?: () => GoalRuntime | undefined;
206
221
  /** Bridge to the connected client (e.g. ACP editor host). Tools should route fs/terminal/permission requests through this when available. */
@@ -312,6 +327,29 @@ export function computeEssentialBuiltinNames(settings: Settings): string[] {
312
327
  * Hindsight memory helpers are intentionally excluded: memory is a private backend
313
328
  * integration, not a public gajae-code tool surface.
314
329
  */
330
+ export interface BuiltinCapabilityCatalogEntry {
331
+ name: string;
332
+ label: string;
333
+ summary: string;
334
+ docsPath: string;
335
+ callableBuiltin: boolean;
336
+ defaultEnabled: boolean;
337
+ }
338
+
339
+ export const BUILTIN_CAPABILITY_CATALOG: readonly BuiltinCapabilityCatalogEntry[] = isComputerLoadablePlatform()
340
+ ? [
341
+ {
342
+ name: "computer",
343
+ label: "Computer",
344
+ summary:
345
+ "Apple Silicon macOS desktop screenshot and input control; enabled by default on supported hosts and supervisor-gated.",
346
+ docsPath: "docs/tools/computer.md",
347
+ callableBuiltin: false,
348
+ defaultEnabled: true,
349
+ },
350
+ ]
351
+ : [];
352
+
315
353
  export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
316
354
  read: s => new ReadTool(s),
317
355
  bash: s => new BashTool(s),
@@ -330,6 +368,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
330
368
  lsp: LspTool.createIf,
331
369
  inspect_image: s => new InspectImageTool(s),
332
370
  browser: s => new BrowserTool(s),
371
+ ...(isComputerLoadablePlatform() ? { computer: ComputerTool.createIf } : {}),
333
372
  checkpoint: CheckpointTool.createIf,
334
373
  rewind: RewindTool.createIf,
335
374
  task: s => TaskTool.create(s),
@@ -504,6 +543,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
504
543
  if (name === "calc") return session.settings.get("calc.enabled");
505
544
  if (name === "skill") return session.settings.get("skill.enabled");
506
545
  if (name === "browser") return session.settings.get("browser.enabled");
546
+ if (name === "computer") return isComputerCallable(session);
507
547
  if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
508
548
  if (name === "irc") {
509
549
  if (!session.settings.get("irc.enabled")) return false;
package/src/tools/job.ts CHANGED
@@ -26,6 +26,7 @@ const jobSchema = z.object({
26
26
  poll: z.array(z.string()).optional().describe("job ids to wait for"),
27
27
  cancel: z.array(z.string()).optional().describe("job ids to cancel"),
28
28
  list: z.boolean().optional().describe("snapshot all jobs"),
29
+ tail: z.array(z.string()).optional().describe("job ids whose retained output should be shown without waiting"),
29
30
  });
30
31
 
31
32
  type JobParams = z.infer<typeof jobSchema>;
@@ -60,9 +61,19 @@ interface CancelOutcome {
60
61
  message: string;
61
62
  }
62
63
 
64
+ export interface JobOutputTail {
65
+ id: string;
66
+ status: AsyncJob["status"];
67
+ text: string;
68
+ startOffset: number;
69
+ nextOffset: number;
70
+ truncated: boolean;
71
+ }
72
+
63
73
  export interface JobToolDetails {
64
74
  jobs: JobSnapshot[];
65
75
  cancelled?: { id: string; status: CancelStatus }[];
76
+ output?: JobOutputTail[];
66
77
  }
67
78
 
68
79
  export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
@@ -107,7 +118,21 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
107
118
  if (params.cancel?.length || params.poll?.length) {
108
119
  throw new ToolError("`list` cannot be combined with `poll` or `cancel`.");
109
120
  }
110
- return this.#buildResult(manager, manager.getAllJobs(ownerFilter), []);
121
+ return this.#buildResult(
122
+ manager,
123
+ manager.getAllJobs(ownerFilter),
124
+ [],
125
+ this.#readOutputTails(manager, params.tail, ownerFilter),
126
+ );
127
+ }
128
+
129
+ if (params.tail?.length && !params.poll?.length && !params.cancel?.length) {
130
+ return this.#buildResult(
131
+ manager,
132
+ this.#visibleJobs(manager, params.tail, ownerId),
133
+ [],
134
+ this.#readOutputTails(manager, params.tail, ownerFilter),
135
+ );
111
136
  }
112
137
 
113
138
  const cancelIds = params.cancel ?? [];
@@ -150,7 +175,12 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
150
175
 
151
176
  if (!shouldPoll) {
152
177
  const cancelledJobs = this.#visibleJobs(manager, cancelIds, ownerId);
153
- return this.#buildResult(manager, cancelledJobs, cancelOutcomes);
178
+ return this.#buildResult(
179
+ manager,
180
+ cancelledJobs,
181
+ cancelOutcomes,
182
+ this.#readOutputTails(manager, params.tail, ownerFilter),
183
+ );
154
184
  }
155
185
 
156
186
  // Resolve which jobs to watch.
@@ -163,7 +193,12 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
163
193
  if (jobsToWatch.length === 0) {
164
194
  if (cancelOutcomes.length > 0) {
165
195
  const cancelledJobs = this.#visibleJobs(manager, cancelIds, ownerId);
166
- return this.#buildResult(manager, cancelledJobs, cancelOutcomes);
196
+ return this.#buildResult(
197
+ manager,
198
+ cancelledJobs,
199
+ cancelOutcomes,
200
+ this.#readOutputTails(manager, params.tail, ownerFilter),
201
+ );
167
202
  }
168
203
  const message = requestedPollIds?.length
169
204
  ? `No matching jobs found for IDs: ${requestedPollIds.join(", ")}`
@@ -178,7 +213,12 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
178
213
  const runningJobs = jobsToWatch.filter(j => j.status === "running");
179
214
  if (runningJobs.length === 0) {
180
215
  const cancelledJobs = cancelIds.map(id => manager.getJob(id)).filter(j => j != null);
181
- return this.#buildResult(manager, [...cancelledJobs, ...jobsToWatch], cancelOutcomes);
216
+ return this.#buildResult(
217
+ manager,
218
+ [...cancelledJobs, ...jobsToWatch],
219
+ cancelOutcomes,
220
+ this.#readOutputTails(manager, params.tail, ownerFilter),
221
+ );
182
222
  }
183
223
 
184
224
  // Wait until at least one running job finishes, the wait duration elapses, or the call is aborted.
@@ -231,7 +271,12 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
231
271
  if (progressTimer) clearInterval(progressTimer);
232
272
  }
233
273
 
234
- return this.#buildResult(manager, allTrackedJobs, cancelOutcomes);
274
+ return this.#buildResult(
275
+ manager,
276
+ allTrackedJobs,
277
+ cancelOutcomes,
278
+ this.#readOutputTails(manager, params.tail, ownerFilter),
279
+ );
235
280
  }
236
281
 
237
282
  /**
@@ -239,6 +284,28 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
239
284
  * Drops missing ids and ids owned by other agents, so cross-agent inspection
240
285
  * via the `job` tool is impossible.
241
286
  */
287
+ #readOutputTails(
288
+ manager: AsyncJobManager,
289
+ ids: string[] | undefined,
290
+ ownerFilter: { ownerId: string } | undefined,
291
+ ): JobOutputTail[] {
292
+ if (!ids?.length) return [];
293
+ const out: JobOutputTail[] = [];
294
+ for (const id of Array.from(new Set(ids.map(value => value.trim()).filter(Boolean)))) {
295
+ const slice = manager.readOutputSince(id, 0, ownerFilter);
296
+ if (!slice) continue;
297
+ out.push({
298
+ id: slice.jobId,
299
+ status: slice.status,
300
+ text: slice.text,
301
+ startOffset: slice.startOffset,
302
+ nextOffset: slice.nextOffset,
303
+ truncated: slice.truncated,
304
+ });
305
+ }
306
+ return out;
307
+ }
308
+
242
309
  #visibleJobs(manager: AsyncJobManager, ids: string[], ownerId: string | undefined): AsyncJob[] {
243
310
  const out: AsyncJob[] = [];
244
311
  for (const id of ids) {
@@ -290,6 +357,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
290
357
  errorText?: string;
291
358
  }[],
292
359
  cancelOutcomes: CancelOutcome[],
360
+ outputTails: JobOutputTail[] = [],
293
361
  ): AgentToolResult<JobToolDetails> {
294
362
  // Deduplicate by id (cancelled jobs may also appear in the watched set).
295
363
  const seen = new Set<string>();
@@ -335,11 +403,21 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
335
403
  }
336
404
  }
337
405
 
406
+ if (outputTails.length > 0) {
407
+ lines.push("", `## Retained Output (${outputTails.length})\n`);
408
+ for (const tail of outputTails) {
409
+ lines.push(`### ${tail.id} — ${tail.status}`);
410
+ if (tail.truncated) lines.push(`(showing retained tail from byte ${tail.startOffset})`);
411
+ lines.push("```", tail.text || "(no retained output yet)", "```", `cursor: ${tail.nextOffset}`, "");
412
+ }
413
+ }
414
+
338
415
  return {
339
416
  content: [{ type: "text", text: lines.join("\n").trimEnd() }],
340
417
  details: {
341
418
  jobs: jobResults,
342
419
  ...(cancelOutcomes.length ? { cancelled: cancelOutcomes.map(({ id, status }) => ({ id, status })) } : {}),
420
+ ...(outputTails.length ? { output: outputTails } : {}),
343
421
  },
344
422
  };
345
423
  }
@@ -352,6 +430,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
352
430
  interface JobRenderArgs {
353
431
  poll?: string[];
354
432
  cancel?: string[];
433
+ tail?: string[];
355
434
  }
356
435
 
357
436
  const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
@@ -391,6 +470,7 @@ function statusToColor(status: JobSnapshot["status"]): ToolUIColor {
391
470
  function describeTarget(args: JobRenderArgs | undefined): string {
392
471
  const poll = args?.poll ?? [];
393
472
  const cancel = args?.cancel ?? [];
473
+ const tail = args?.tail ?? [];
394
474
  const parts: string[] = [];
395
475
  if (cancel.length > 0) {
396
476
  parts.push(cancel.length === 1 ? `cancel ${cancel[0]}` : `cancel ${cancel.length} jobs`);
@@ -398,6 +478,9 @@ function describeTarget(args: JobRenderArgs | undefined): string {
398
478
  if (poll.length > 0) {
399
479
  parts.push(poll.length === 1 ? `poll ${poll[0]}` : `poll ${poll.length} jobs`);
400
480
  }
481
+ if (tail.length > 0) {
482
+ parts.push(tail.length === 1 ? `tail ${tail[0]}` : `tail ${tail.length} jobs`);
483
+ }
401
484
  if (parts.length === 0) return "all running jobs";
402
485
  return parts.join(", ");
403
486
  }
@@ -81,6 +81,25 @@ function buildTreePrefix(theme: Theme, ancestors: readonly boolean[]): string {
81
81
  return ancestors.map(hasNext => (hasNext ? `${theme.tree.vertical} ` : " ")).join("");
82
82
  }
83
83
 
84
+ function splitByDisplayWidth(value: string, maxWidth: number): string[] {
85
+ const width = Math.max(1, maxWidth);
86
+ const fragments: string[] = [];
87
+ let current = "";
88
+ let currentWidth = 0;
89
+ for (const char of value) {
90
+ const charWidth = Math.max(1, Bun.stringWidth(char));
91
+ if (current && currentWidth + charWidth > width) {
92
+ fragments.push(current);
93
+ current = "";
94
+ currentWidth = 0;
95
+ }
96
+ current += char;
97
+ currentWidth += charWidth;
98
+ }
99
+ if (current || fragments.length === 0) fragments.push(current);
100
+ return fragments;
101
+ }
102
+
84
103
  /**
85
104
  * Render a JSON value as tree lines.
86
105
  */
@@ -121,38 +140,32 @@ export function renderJsonTreeLines(
121
140
  // Handle scalars
122
141
  if (val === null || val === undefined || typeof val !== "object") {
123
142
  const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
124
-
125
- // Special handling for multiline strings
126
- if (typeof val === "string" && val.includes("\n")) {
127
- const strLines = val.split("\n");
128
- const maxStrLines = Math.min(strLines.length, Math.max(1, maxLines - lines.length - 1));
143
+ if (typeof val === "string") {
144
+ const escaped = val.replace(/\n/g, "\\n").replace(/\t/g, "\\t");
145
+ const marker = "";
146
+ const firstFragmentWidth = Math.max(1, maxScalarLen - 2);
147
+ const continuationWidth = Math.max(1, maxScalarLen - Bun.stringWidth(marker) - 1);
148
+ const firstFragments = splitByDisplayWidth(escaped, firstFragmentWidth);
149
+ const firstFragment = firstFragments[0] ?? "";
150
+ const remainder = firstFragments.slice(1).join("");
151
+ const continuationFragments = remainder ? splitByDisplayWidth(remainder, continuationWidth) : [];
152
+ const fragments = [firstFragment, ...continuationFragments];
153
+ const lineBudget = Math.max(1, maxLines - lines.length);
154
+ const visibleCount = Math.min(fragments.length, lineBudget);
129
155
  const continuePrefix = buildTreePrefix(theme, ancestors);
156
+ const isStringTruncated = visibleCount < fragments.length;
130
157
 
131
- // First line with label
132
- const firstLine = truncateToWidth(strLines[0], maxScalarLen);
133
- pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", `"${firstLine}`)}`);
134
-
135
- // Subsequent lines indented
136
- for (let i = 1; i < maxStrLines; i++) {
137
- if (lines.length >= maxLines) {
138
- truncated = true;
139
- break;
140
- }
141
- const line = truncateToWidth(strLines[i], maxScalarLen);
142
- pushLine(`${continuePrefix} ${theme.fg("dim", ` ${line}`)}`);
143
- }
144
-
145
- // Show truncation and closing quote
146
- if (strLines.length > maxStrLines) {
147
- truncated = true;
148
- pushLine(
149
- `${continuePrefix} ${theme.fg("dim", ` …(${strLines.length - maxStrLines} more lines)"`)}`,
150
- );
151
- } else {
152
- // Add closing quote to last line - need to modify the last pushed line
153
- const lastIdx = lines.length - 1;
154
- lines[lastIdx] = `${lines[lastIdx]}${theme.fg("dim", '"')}`;
158
+ for (let i = 0; i < visibleCount; i++) {
159
+ const isFirst = i === 0;
160
+ const isFinalVisible = i === visibleCount - 1;
161
+ const suffix = isStringTruncated && isFinalVisible ? `${marker}"` : isFinalVisible ? '"' : "";
162
+ const rendered = isFirst ? `"${fragments[i]}${suffix}` : `↳ ${fragments[i]}${suffix}`;
163
+ const line = isFirst
164
+ ? `${prefix}${iconScalar} ${label}: ${theme.fg("dim", rendered)}`
165
+ : `${continuePrefix} ${theme.fg("dim", rendered)}`;
166
+ if (!pushLine(line)) break;
155
167
  }
168
+ if (isStringTruncated) truncated = true;
156
169
  return;
157
170
  }
158
171
 
@@ -17,6 +17,7 @@ import { astGrepToolRenderer } from "./ast-grep";
17
17
  import { bashToolRenderer } from "./bash";
18
18
  import { browserToolRenderer } from "./browser/render";
19
19
  import { calculatorToolRenderer } from "./calculator";
20
+ import { computerToolRenderer } from "./computer/render";
20
21
  import { debugToolRenderer } from "./debug";
21
22
  import { evalToolRenderer } from "./eval";
22
23
  import { findToolRenderer } from "./find";
@@ -52,6 +53,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
52
53
  ast_edit: astEditToolRenderer as ToolRenderer,
53
54
  bash: bashToolRenderer as ToolRenderer,
54
55
  browser: browserToolRenderer as ToolRenderer,
56
+ computer: computerToolRenderer as ToolRenderer,
55
57
  recipe: recipeToolRenderer as ToolRenderer,
56
58
  debug: debugToolRenderer as ToolRenderer,
57
59
  eval: evalToolRenderer as ToolRenderer,
@@ -11,6 +11,7 @@ export const TOOL_TIMEOUTS = {
11
11
  bash: { default: 300, min: 1, max: 3600 },
12
12
  eval: { default: 30, min: 1, max: 600 },
13
13
  browser: { default: 30, min: 1, max: 300 },
14
+ computer: { default: 30, min: 1, max: 300 },
14
15
  ssh: { default: 60, min: 1, max: 3600 },
15
16
  fetch: { default: 20, min: 1, max: 45 },
16
17
  lsp: { default: 20, min: 5, max: 60 },
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Unified Web Search Tool
3
3
  *
4
- * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, OpenAI code backend, Tavily, Kagi, Z.AI, SearXNG, and Synthetic
4
+ * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, OpenAI code backend, xAI, Tavily, Kagi, Z.AI, SearXNG, and Synthetic
5
5
  * providers with provider-specific parameters exposed conditionally.
6
6
  */
7
7
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
@@ -29,6 +29,20 @@ export const webSearchSchema = z.object({
29
29
  max_tokens: z.number().describe("max output tokens").optional(),
30
30
  temperature: z.number().describe("sampling temperature").optional(),
31
31
  num_search_results: z.number().describe("number of search results").optional(),
32
+ xai_search_mode: z
33
+ .enum(["web", "x", "web_and_x"])
34
+ .describe("xAI only: use web_search, x_search, or both")
35
+ .optional(),
36
+ allowed_domains: z.array(z.string()).max(5).describe("xAI web_search only: allowed domains").optional(),
37
+ excluded_domains: z.array(z.string()).max(5).describe("xAI web_search only: excluded domains").optional(),
38
+ allowed_x_handles: z.array(z.string()).max(20).describe("xAI x_search only: allowed X handles").optional(),
39
+ excluded_x_handles: z.array(z.string()).max(20).describe("xAI x_search only: excluded X handles").optional(),
40
+ from_date: z.string().describe("xAI x_search only: start date in ISO8601 format").optional(),
41
+ to_date: z.string().describe("xAI x_search only: end date in ISO8601 format").optional(),
42
+ enable_image_understanding: z.boolean().describe("xAI only: analyze images encountered during search").optional(),
43
+ enable_image_search: z.boolean().describe("xAI web_search only: search for and embed image results").optional(),
44
+ enable_video_understanding: z.boolean().describe("xAI x_search only: analyze videos in X posts").optional(),
45
+ no_inline_citations: z.boolean().describe("xAI only: disable inline citation markdown in the answer").optional(),
32
46
  });
33
47
 
34
48
  export type SearchToolParams = z.infer<typeof webSearchSchema>;
@@ -153,6 +167,17 @@ async function executeSearch(
153
167
  maxOutputTokens: params.max_tokens,
154
168
  numSearchResults: params.num_search_results,
155
169
  temperature: params.temperature,
170
+ xaiSearchMode: params.xai_search_mode,
171
+ allowedDomains: params.allowed_domains,
172
+ excludedDomains: params.excluded_domains,
173
+ allowedXHandles: params.allowed_x_handles,
174
+ excludedXHandles: params.excluded_x_handles,
175
+ fromDate: params.from_date,
176
+ toDate: params.to_date,
177
+ enableImageUnderstanding: params.enable_image_understanding,
178
+ enableImageSearch: params.enable_image_search,
179
+ enableVideoUnderstanding: params.enable_video_understanding,
180
+ noInlineCitations: params.no_inline_citations,
156
181
  signal,
157
182
  authStorage,
158
183
  sessionId,
@@ -225,7 +250,7 @@ export async function runSearchQuery(
225
250
  /**
226
251
  * Web search tool implementation.
227
252
  *
228
- * Supports Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, OpenAI code backend, Z.AI, SearXNG, and Synthetic providers with automatic fallback.
253
+ * Supports Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, OpenAI code backend, xAI, Z.AI, SearXNG, and Synthetic providers with automatic fallback.
229
254
  */
230
255
  export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
231
256
  readonly name = "web_search";
@@ -45,6 +45,7 @@ const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
45
45
  load: async () => new (await import("./providers/gemini")).GeminiProvider(),
46
46
  },
47
47
  codex: { id: "codex", label: "OpenAI", load: async () => new (await import("./providers/codex")).CodexProvider() },
48
+ xai: { id: "xai", label: "xAI", load: async () => new (await import("./providers/xai")).XaiProvider() },
48
49
  tavily: {
49
50
  id: "tavily",
50
51
  label: "Tavily",
@@ -104,6 +105,7 @@ export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
104
105
  "anthropic",
105
106
  "gemini",
106
107
  "codex",
108
+ "xai",
107
109
  "zai",
108
110
  "exa",
109
111
  "parallel",
@@ -116,6 +118,7 @@ const MODEL_PROVIDER_TO_SEARCH: Record<string, SearchProviderId> = {
116
118
  openai: "codex",
117
119
  "openai-codex": "codex",
118
120
  "openai-responses": "codex",
121
+ xai: "xai",
119
122
  anthropic: "anthropic",
120
123
  google: "gemini",
121
124
  "google-gemini-cli": "gemini",
@@ -185,6 +188,16 @@ function looksOpenAIFamilyModelId(ctx: ActiveSearchModelContext): boolean {
185
188
  return looksHostedModelId(ctx.wireModelId) || looksHostedModelId(ctx.modelId);
186
189
  }
187
190
 
191
+ function looksXaiModelId(modelId: string | undefined): boolean {
192
+ if (!modelId) return false;
193
+ const id = modelId.toLowerCase();
194
+ return id.startsWith("grok-") || id.startsWith("x-ai/grok-") || id.startsWith("xai/grok-");
195
+ }
196
+
197
+ function looksXaiFamilyModelId(ctx: ActiveSearchModelContext): boolean {
198
+ return looksXaiModelId(ctx.wireModelId) || looksXaiModelId(ctx.modelId);
199
+ }
200
+
188
201
  export function isLocalBaseUrl(baseUrl: string | undefined): boolean {
189
202
  if (!baseUrl) return false;
190
203
  let url: URL;
@@ -226,6 +239,7 @@ export function inferNativeProviderFromModel(ctx: ActiveSearchModelContext | und
226
239
  const modelId = (ctx.wireModelId ?? ctx.modelId).toLowerCase();
227
240
  if (modelId.startsWith("claude-") && isAnthropicWire(ctx.api)) return "anthropic";
228
241
  if (modelId.startsWith("gemini-") && isGoogleWire(ctx.api)) return "gemini";
242
+ if (looksXaiFamilyModelId(ctx) && isOpenAICompatWire(ctx.api)) return "xai";
229
243
  if (looksOpenAIFamilyModelId(ctx) && isOpenAICompatWire(ctx.api)) {
230
244
  if (ctx.webSearch === "on" || !isLocalBaseUrl(ctx.baseUrl)) return "codex";
231
245
  }
@@ -290,7 +304,8 @@ export async function resolveProviderChain(options: ResolveProviderChainOptions)
290
304
  await appendAvailable(chain, directId, authStorage);
291
305
  const inferred = inferNativeProviderFromModel(activeModelContext);
292
306
  if (inferred) await appendAvailable(chain, inferred, authStorage);
293
- if (await shouldTryGenericOpenAICompat(authStorage, activeModelContext, sessionId, signal))
307
+ const hasNativeXai = chain.includes("xai");
308
+ if (!hasNativeXai && (await shouldTryGenericOpenAICompat(authStorage, activeModelContext, sessionId, signal)))
294
309
  appendDeduped(chain, "openai-compatible");
295
310
  }
296
311
 
@@ -33,6 +33,28 @@ export interface SearchParams {
33
33
  maxOutputTokens?: number;
34
34
  numSearchResults?: number;
35
35
  temperature?: number;
36
+ /** xAI-specific search surface. Defaults to web_search when omitted. */
37
+ xaiSearchMode?: "web" | "x" | "web_and_x";
38
+ /** xAI web_search domain allow-list (max 5). */
39
+ allowedDomains?: string[];
40
+ /** xAI web_search domain deny-list (max 5). */
41
+ excludedDomains?: string[];
42
+ /** xAI x_search handle allow-list (max 20). */
43
+ allowedXHandles?: string[];
44
+ /** xAI x_search handle deny-list (max 20). */
45
+ excludedXHandles?: string[];
46
+ /** xAI x_search lower date bound, ISO8601 date such as YYYY-MM-DD. */
47
+ fromDate?: string;
48
+ /** xAI x_search upper date bound, ISO8601 date such as YYYY-MM-DD. */
49
+ toDate?: string;
50
+ /** xAI web_search/x_search image understanding. */
51
+ enableImageUnderstanding?: boolean;
52
+ /** xAI web_search image search result embedding. */
53
+ enableImageSearch?: boolean;
54
+ /** xAI x_search video understanding. */
55
+ enableVideoUnderstanding?: boolean;
56
+ /** xAI Responses include=["no_inline_citations"]. */
57
+ noInlineCitations?: boolean;
36
58
  googleSearch?: Record<string, unknown>;
37
59
  codeExecution?: Record<string, unknown>;
38
60
  urlContext?: Record<string, unknown>;