tesseract-agent 0.4.19 → 0.4.21

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 (2) hide show
  1. package/dist-bundle/cli.js +503 -231
  2. package/package.json +1 -1
@@ -19450,6 +19450,10 @@ var init_ws_protocol = __esm({
19450
19450
  // co-viewers see images the moment the agent generates them
19451
19451
  "agent:canvas_ready",
19452
19452
  // co-viewers dock the HTML canvas the moment the agent renders it
19453
+ "agent:report_ready",
19454
+ // co-viewers see the mission report (and its in-place updates)
19455
+ "agent:mission_progress",
19456
+ // co-viewers see the Mission card's live stage progress
19453
19457
  "agent:elicitation_complete",
19454
19458
  // M5 — co-viewers clear a resolved url-mode card
19455
19459
  "chat:echoed",
@@ -19487,6 +19491,7 @@ var init_plans = __esm({
19487
19491
  advancedBrowser: false,
19488
19492
  hosting: false,
19489
19493
  plugins: false,
19494
+ missions: false,
19490
19495
  scheduling: false,
19491
19496
  api: false,
19492
19497
  sso: false,
@@ -19523,6 +19528,7 @@ var init_plans = __esm({
19523
19528
  "Tesseract cloud server \u2014 a managed always-on agent in the cloud",
19524
19529
  "Deploy to the cloud \u2014 your app on a *.tesseract.dev URL",
19525
19530
  "Includes ~1 always-on service + 20 GB storage + 100 GB bandwidth (overage metered)",
19531
+ "Premium missions \u2014 GDPR, security, UX, performance audits that fix & verify",
19526
19532
  "Sync settings across your devices",
19527
19533
  "Email support"
19528
19534
  ],
@@ -19536,6 +19542,7 @@ var init_plans = __esm({
19536
19542
  voice: true,
19537
19543
  hosting: true,
19538
19544
  plugins: true,
19545
+ missions: true,
19539
19546
  scheduling: true,
19540
19547
  maxMachines: 0
19541
19548
  },
@@ -19570,6 +19577,7 @@ var init_plans = __esm({
19570
19577
  advancedBrowser: true,
19571
19578
  hosting: true,
19572
19579
  plugins: true,
19580
+ missions: true,
19573
19581
  scheduling: true,
19574
19582
  api: true,
19575
19583
  sso: true,
@@ -20407,19 +20415,19 @@ function tokensToTailwindTheme(input) {
20407
20415
  const tokens = normalizeTokens(input);
20408
20416
  const extend2 = {};
20409
20417
  for (const { path: path5, token } of walkTokens(tokens)) {
20410
- const bucket = TW_BUCKETS.find((b) => b.match.length <= path5.length && b.match.every((seg, i) => path5[i] === seg));
20411
- if (!bucket)
20418
+ const bucket2 = TW_BUCKETS.find((b) => b.match.length <= path5.length && b.match.every((seg, i) => path5[i] === seg));
20419
+ if (!bucket2)
20412
20420
  continue;
20413
- const rest = path5.slice(bucket.match.length).map(kebab).filter(Boolean);
20421
+ const rest = path5.slice(bucket2.match.length).map(kebab).filter(Boolean);
20414
20422
  if (rest.length === 0)
20415
20423
  continue;
20416
20424
  const ref = `var(${tokenCssVarName(path5, token)})`;
20417
- if (!extend2[bucket.key])
20418
- extend2[bucket.key] = {};
20419
- if (bucket.flat)
20420
- extend2[bucket.key][rest.join("-")] = ref;
20425
+ if (!extend2[bucket2.key])
20426
+ extend2[bucket2.key] = {};
20427
+ if (bucket2.flat)
20428
+ extend2[bucket2.key][rest.join("-")] = ref;
20421
20429
  else
20422
- setNested(extend2[bucket.key], rest, ref);
20430
+ setNested(extend2[bucket2.key], rest, ref);
20423
20431
  }
20424
20432
  return { extend: extend2 };
20425
20433
  }
@@ -21440,7 +21448,7 @@ function slugify3(s) {
21440
21448
  }
21441
21449
  function buildCanvasTools(deps) {
21442
21450
  return [
21443
- tool4("render_canvas", "Render an HTML document as a live, sandboxed Canvas docked beside the chat (like OpenAI Canvas), for INTERACTIVE or multi-part PROJECTS (apps, tools, dashboards, anything the user will interact with at size). For a SMALL self-contained snippet or animation, do NOT use this \u2014 instead just include it as a fenced ```html (or ```svg) code block in your reply; it renders live inline in the message. To TRANSFORM an existing canvas, call again with the SAME id and the full updated HTML \u2014 it re-renders in place. Returns the canvas file path.", {
21451
+ tool4("render_canvas", "Render an HTML document as a live, sandboxed Canvas docked beside the chat (like OpenAI Canvas), for INTERACTIVE or multi-part PROJECTS (apps, tools, dashboards, anything the user will interact with at size). For a SMALL self-contained snippet or animation, do NOT use this \u2014 instead just include it as a fenced ```html (or ```svg) code block in your reply; it renders live inline in the message. To TRANSFORM an existing canvas, call again with the SAME id and the full updated HTML \u2014 it re-renders in place. Before composing the HTML, consult the canvas-design skill for design fundamentals (and chart-design / diagram-design when the canvas is a chart or a diagram) so the result looks deliberately designed. Returns the canvas file path.", {
21444
21452
  html: external_exports.string().describe("The full HTML document to render (include <html>/<body>; put CSS/JS inline)."),
21445
21453
  title: external_exports.string().optional().describe("Short title for the canvas tab."),
21446
21454
  id: external_exports.string().optional().describe("Stable id to UPDATE an existing canvas; omit to create a new one.")
@@ -21462,6 +21470,132 @@ var init_canvas_tools = __esm({
21462
21470
  }
21463
21471
  });
21464
21472
 
21473
+ // ../../packages/agent-core/dist/report-tools.js
21474
+ import { tool as tool5 } from "@anthropic-ai/claude-agent-sdk";
21475
+ function slug(s) {
21476
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "mission";
21477
+ }
21478
+ function buildReportTools(deps) {
21479
+ return [
21480
+ tool5("emit_report", `Deliver the structured findings of a mission playbook run as a Report card in the chat (this is the deliverable \u2014 do NOT just write findings as prose). Call it ONCE near the end with all findings grouped into the playbook's sections. To UPDATE after fixing/verifying a finding (Fix / Re-run), call again with the SAME id and finding ids, changing each finding's status \u2014 it re-renders in place. For "legal" bucket reports, never claim the site is compliant/certified; describe risks and route the verdict to a human.`, {
21481
+ id: external_exports.string().optional().describe("Stable report id to UPDATE an existing report; omit on the first call to create one."),
21482
+ playbookId: external_exports.string().describe("The playbook id this report belongs to."),
21483
+ title: external_exports.string().describe("Report title (usually the playbook name)."),
21484
+ summary: external_exports.string().describe("Plain-language overview of what was found."),
21485
+ bucket: bucket.describe("Overall verifiability bucket for the report."),
21486
+ sections: external_exports.array(external_exports.object({ id: external_exports.string(), title: external_exports.string(), description: external_exports.string().optional() })).describe("Ordered layout sections; findings reference these by id."),
21487
+ findings: external_exports.array(finding)
21488
+ }, async (args) => {
21489
+ const id = args.id ? slug(args.id) : `report-${slug(args.playbookId)}-${Date.now()}`;
21490
+ const report = {
21491
+ id,
21492
+ playbookId: args.playbookId,
21493
+ title: args.title,
21494
+ summary: args.summary,
21495
+ bucket: args.bucket,
21496
+ sections: args.sections,
21497
+ findings: args.findings,
21498
+ ts: Date.now()
21499
+ };
21500
+ deps.emitReport(report);
21501
+ return {
21502
+ content: [
21503
+ {
21504
+ type: "text",
21505
+ text: `Report "${args.title}" delivered (${args.findings.length} finding(s)). To update a finding's status after fixing it, call emit_report again with id "${id}" and the same finding ids.`
21506
+ }
21507
+ ]
21508
+ };
21509
+ })
21510
+ ];
21511
+ }
21512
+ var bucket, finding;
21513
+ var init_report_tools = __esm({
21514
+ "../../packages/agent-core/dist/report-tools.js"() {
21515
+ "use strict";
21516
+ init_zod();
21517
+ bucket = external_exports.enum(["deterministic", "heuristic", "legal"]);
21518
+ finding = external_exports.object({
21519
+ id: external_exports.string().describe("Stable id for this finding \u2014 REUSE it across updates so status changes land in place."),
21520
+ severity: external_exports.enum(["high", "medium", "low", "info"]),
21521
+ title: external_exports.string().describe("Short headline."),
21522
+ description: external_exports.string().describe("Plain-language explanation a non-technical user understands."),
21523
+ section: external_exports.string().optional().describe("Id of the report section this belongs under (matches a sections[].id)."),
21524
+ location: external_exports.object({ selector: external_exports.string().optional(), xpath: external_exports.string().optional(), file: external_exports.string().optional(), line: external_exports.number().optional() }).optional().describe("Where it is \u2014 a CSS selector/xpath on the page, or a source file+line."),
21525
+ suggestedFix: external_exports.string().optional().describe("Plain-language suggested fix."),
21526
+ bucket: bucket.describe("Verifiability: deterministic (auto-fixable+verifiable), heuristic (advisory), or legal (readiness only \u2014 never certify)."),
21527
+ status: external_exports.enum(["open", "fixed", "wontfix"]).describe('Current status; start as "open".')
21528
+ });
21529
+ }
21530
+ });
21531
+
21532
+ // ../../packages/agent-core/dist/interactive-ui-tools.js
21533
+ import { tool as tool6 } from "@anthropic-ai/claude-agent-sdk";
21534
+ function buildInteractiveUiTools(deps) {
21535
+ return [
21536
+ tool6("ask_form", "Show the user a FORM in the chat and wait for them to fill it in. Use this to collect structured input mid-task (settings, scope, details) instead of asking in prose. Returns the submitted values (or that the user cancelled). Prefer this over free-text questions when the answer is structured.", {
21537
+ title: external_exports.string().describe("Form heading (plain language)."),
21538
+ description: external_exports.string().optional().describe("Short explanation shown above the fields."),
21539
+ submitLabel: external_exports.string().optional().describe('Submit button label (default "Submit").'),
21540
+ fields: external_exports.array(formField).min(1),
21541
+ frozen: external_exports.boolean().optional().describe("Pin this card to the top of the chat so it does not scroll away while you keep working; it unpins once answered. Use true for input you NEED answered.")
21542
+ }, async (args) => {
21543
+ const spec = { component: "form", title: args.title, description: args.description, submitLabel: args.submitLabel, fields: args.fields, frozen: args.frozen };
21544
+ const res = await deps.raiseUiRequest(spec);
21545
+ if (res.cancelled)
21546
+ return { content: [{ type: "text", text: "The user cancelled the form (no values submitted)." }] };
21547
+ return { content: [{ type: "text", text: `The user submitted the form:
21548
+ ${JSON.stringify(res.values ?? {}, null, 2)}` }] };
21549
+ }),
21550
+ tool6("present_choices", "Show the user a CHOICE picker in the chat and wait for them to choose. Use this to let the user make a decision or pick from options (single or multi-select). Returns the selected value(s) (or cancellation).", {
21551
+ title: external_exports.string().describe("The question / heading."),
21552
+ description: external_exports.string().optional(),
21553
+ options: external_exports.array(choiceOption).min(1),
21554
+ multiSelect: external_exports.boolean().optional().describe("Allow selecting more than one."),
21555
+ frozen: external_exports.boolean().optional().describe("Pin this card to the top of the chat so it does not scroll away while you keep working; it unpins once answered.")
21556
+ }, async (args) => {
21557
+ const spec = { component: "choices", title: args.title, description: args.description, options: args.options, multiSelect: args.multiSelect, frozen: args.frozen };
21558
+ const res = await deps.raiseUiRequest(spec);
21559
+ if (res.cancelled)
21560
+ return { content: [{ type: "text", text: "The user cancelled the choice (nothing selected)." }] };
21561
+ return { content: [{ type: "text", text: `The user selected: ${JSON.stringify(res.selected ?? [])}` }] };
21562
+ }),
21563
+ tool6("update_progress", `Update the live Mission card so the user sees ongoing status. Call this whenever you move between stages (and once at the start). Pass the FULL ordered stage list each time with each stage's current status, plus a short "what I'm doing now" note. This is a one-way feedback channel \u2014 it returns immediately, does not wait for the user.`, {
21564
+ stages: external_exports.array(external_exports.object({
21565
+ label: external_exports.string(),
21566
+ status: external_exports.enum(["pending", "active", "done"])
21567
+ })).min(1).describe("The full ordered list of stages with each one's current status."),
21568
+ note: external_exports.string().optional().describe("A short present-tense line about what is happening right now.")
21569
+ }, async (args) => {
21570
+ deps.emitProgress(args.stages, args.note);
21571
+ return { content: [{ type: "text", text: "Progress updated." }] };
21572
+ })
21573
+ ];
21574
+ }
21575
+ var formField, choiceOption;
21576
+ var init_interactive_ui_tools = __esm({
21577
+ "../../packages/agent-core/dist/interactive-ui-tools.js"() {
21578
+ "use strict";
21579
+ init_zod();
21580
+ formField = external_exports.object({
21581
+ key: external_exports.string().describe("Stable key the answer is returned under."),
21582
+ label: external_exports.string(),
21583
+ help: external_exports.string().optional().describe("Help text shown under the field."),
21584
+ kind: external_exports.enum(["text", "textarea", "select", "multiselect", "toggle", "number"]),
21585
+ options: external_exports.array(external_exports.object({ value: external_exports.string(), label: external_exports.string() })).optional().describe("For select / multiselect."),
21586
+ default: external_exports.union([external_exports.string(), external_exports.array(external_exports.string()), external_exports.boolean(), external_exports.number()]).optional(),
21587
+ required: external_exports.boolean().optional(),
21588
+ placeholder: external_exports.string().optional()
21589
+ });
21590
+ choiceOption = external_exports.object({
21591
+ value: external_exports.string(),
21592
+ label: external_exports.string(),
21593
+ description: external_exports.string().optional(),
21594
+ preview: external_exports.string().optional().describe("Optional markdown preview shown when this option is focused.")
21595
+ });
21596
+ }
21597
+ });
21598
+
21465
21599
  // ../../packages/agent-core/dist/session-transfer.js
21466
21600
  import { homedir as homedir2 } from "node:os";
21467
21601
  import { join as join6, dirname as dirname3, sep, resolve as resolve3 } from "node:path";
@@ -21634,6 +21768,21 @@ async function appendCanvasArtifact(sessionId, projectDir, a) {
21634
21768
  await writeManifest(dir, list);
21635
21769
  });
21636
21770
  }
21771
+ async function appendReportArtifact(sessionId, projectDir, report) {
21772
+ const dir = await sessionSidecarDir(sessionId, projectDir);
21773
+ if (!dir)
21774
+ return;
21775
+ await withManifestLock(dir, async () => {
21776
+ const list = await readManifest(dir);
21777
+ const entry = { id: `report-${report.id}`, kind: "report", ts: report.ts, title: report.title, report };
21778
+ const at = list.findIndex((x) => x.kind === "report" && x.report?.id === report.id);
21779
+ if (at >= 0)
21780
+ list[at] = entry;
21781
+ else
21782
+ list.push(entry);
21783
+ await writeManifest(dir, list);
21784
+ });
21785
+ }
21637
21786
  async function readSessionArtifacts(sessionId, projectDir) {
21638
21787
  const dir = await sessionSidecarDir(sessionId, projectDir);
21639
21788
  if (!dir)
@@ -21649,7 +21798,8 @@ async function readSessionArtifacts(sessionId, projectDir) {
21649
21798
  images: a.images?.filter((im) => inDir(im.rel)).map((im) => ({ path: toAbs(dir, im.rel), mime: im.mime })),
21650
21799
  canvasId: a.canvasId,
21651
21800
  title: a.title,
21652
- path: a.file && inDir(a.file) ? toAbs(dir, a.file) : void 0
21801
+ path: a.file && inDir(a.file) ? toAbs(dir, a.file) : void 0,
21802
+ report: a.report
21653
21803
  })).sort((x, y) => x.ts - y.ts);
21654
21804
  }
21655
21805
  var MANIFEST, toRel, toAbs, manifestLocks;
@@ -22566,6 +22716,8 @@ var init_agent_manager = __esm({
22566
22716
  init_design_tools();
22567
22717
  init_imagegen_tools();
22568
22718
  init_canvas_tools();
22719
+ init_report_tools();
22720
+ init_interactive_ui_tools();
22569
22721
  init_session_artifacts();
22570
22722
  init_design_tokens2();
22571
22723
  init_narrator();
@@ -22620,7 +22772,7 @@ var init_agent_manager = __esm({
22620
22772
  };
22621
22773
  }
22622
22774
  };
22623
- IDLE_REAP_MS = 5 * 60 * 1e3;
22775
+ IDLE_REAP_MS = 15 * 60 * 1e3;
22624
22776
  MODERATE_IDLE_MS = 60 * 1e3;
22625
22777
  STEER_COALESCE_MS = 500;
22626
22778
  STOP_HARD_KILL_AFTER_MS = 1200;
@@ -22635,6 +22787,17 @@ var init_agent_manager = __esm({
22635
22787
  liveSessions = /* @__PURE__ */ new Map();
22636
22788
  /** Sessions whose canonical sessionId is not yet known (first frame pending) */
22637
22789
  pendingLiveSessions = /* @__PURE__ */ new Set();
22790
+ /** PRE-WARM (tab-driven): a single standby NEW-session instance — its cli.js process
22791
+ * is already spawned + booted and idling on an empty pump, so the next NEW conversation
22792
+ * whose construction-config matches `warmEmptyKey` is adopted instantly (skips the
22793
+ * ~1.6–3.4s spawn/boot). system/init only fires on the first push, so an un-adopted
22794
+ * warm instance stays silent and writes no transcript. */
22795
+ warmEmpty = null;
22796
+ warmEmptyKey = null;
22797
+ /** The session backing the currently-FOCUSED tab — pinned against idle reap via the
22798
+ * 'tab' keep-alive holder. Switching tabs releases the previous one (arming its 15-min
22799
+ * idle clock) and pins the new one (see setActiveSession). */
22800
+ activeTabSession = null;
22638
22801
  /** requestId → the originating browser-cli socket, for unicast responses.
22639
22802
  * browser-cli is one-shot, so the entry is deleted on the first write. */
22640
22803
  pendingResponses = /* @__PURE__ */ new Map();
@@ -22650,6 +22813,11 @@ var init_agent_manager = __esm({
22650
22813
  /** M5 H6 — parked MCP elicitation prompts, resolved when the client replies via
22651
22814
  * chat:elicitation (→ respondToElicitation) or denied on abort. */
22652
22815
  pendingElicitations = /* @__PURE__ */ new Map();
22816
+ /** Skill-driven interactive UI requests (ask_form / present_choices), resolved when
22817
+ * the client replies via chat:ui_response (→ respondToUiRequest) or the owning
22818
+ * session is torn down (→ cancelled). Keyed by requestId; `sessionId` lets a
22819
+ * teardown sweep cancel a session's parked cards. */
22820
+ pendingUiRequests = /* @__PURE__ */ new Map();
22653
22821
  browserCliPath;
22654
22822
  /** Plugin-contributed agent tools (Epic G). The host loads enabled plugins and sets
22655
22823
  * these; at session start they're aggregated behind ONE in-process MCP server and
@@ -22665,6 +22833,9 @@ var init_agent_manager = __esm({
22665
22833
  /** Local plugin bundling the enabled ClawHub skills (Epic H), loaded via query()'s
22666
22834
  * `plugins` option so the SDK discovers their SKILL.md files. */
22667
22835
  skillsPlugin = null;
22836
+ /** Bundled first-party mission-skills plugin (Part B). Attached per-session only
22837
+ * when a turn carries missionSkillId. Set once by the host from app resources. */
22838
+ missionSkillsPlugin = null;
22668
22839
  /** Host-injected image-generation backend (keys + provider call + disk save live
22669
22840
  * in the host; secrets never reach agent-core). Non-null ⇒ NEW conversations get
22670
22841
  * the in-process 'tesseract-imagegen' tools. */
@@ -22688,6 +22859,12 @@ var init_agent_manager = __esm({
22688
22859
  setSkillsPlugin(config2) {
22689
22860
  this.skillsPlugin = config2;
22690
22861
  }
22862
+ /** Set (or clear) the bundled MISSION-skills local-plugin (first-party mission
22863
+ * procedures). Attached only to sessions launched with a missionSkillId, so it
22864
+ * doesn't bloat normal chats and stays out of the user's Skills list. */
22865
+ setMissionSkillsPlugin(config2) {
22866
+ this.missionSkillsPlugin = config2;
22867
+ }
22691
22868
  /** Inject (or clear) the host's image-generation backend. When set, NEW
22692
22869
  * conversations get the in-process 'tesseract-imagegen' tools. The runner is
22693
22870
  * read fresh per call, so a key the user adds later applies without a restart. */
@@ -22937,6 +23114,41 @@ ${full.slice(-8e3)}`;
22937
23114
  entry.cleanup();
22938
23115
  entry.resolve({ action: decision.action, ...decision.action === "accept" && decision.content ? { content: decision.content } : {} });
22939
23116
  }
23117
+ /** Skill UI: park a promise, raise an interactive card (agent:ui_request), and
23118
+ * resolve it from chat:ui_response (respondToUiRequest). A session teardown
23119
+ * cancels any still-parked cards (cancelSessionUiRequests). Mirrors onElicitation. */
23120
+ raiseUiRequest(live, spec) {
23121
+ return new Promise((resolve10) => {
23122
+ const requestId = randomUUID2();
23123
+ this.pendingUiRequests.set(requestId, { resolve: resolve10, sessionId: live.sessionId, origin: live.origin });
23124
+ const evt = { type: "agent:ui_request", sessionId: live.sessionId, requestId, spec, ts: Date.now() };
23125
+ if (!live.sessionId && live.clientRequestId)
23126
+ this.send({ ...evt, clientRequestId: live.clientRequestId }, live.origin);
23127
+ else
23128
+ this.send(evt, live.origin);
23129
+ });
23130
+ }
23131
+ /** Resolve a parked skill-UI request (client replied via chat:ui_response).
23132
+ * Driver-only, same origin guard as respondToElicitation. */
23133
+ respondToUiRequest(requestId, response, callerOrigin) {
23134
+ const entry = this.pendingUiRequests.get(requestId);
23135
+ if (!entry)
23136
+ return;
23137
+ if (entry.origin && callerOrigin && entry.origin !== callerOrigin)
23138
+ return;
23139
+ this.pendingUiRequests.delete(requestId);
23140
+ entry.resolve(response);
23141
+ }
23142
+ /** Cancel (resolve as cancelled) any interactive cards still parked for a session
23143
+ * that's being torn down / aborted, so the awaiting tool calls unblock. */
23144
+ cancelSessionUiRequests(sessionId) {
23145
+ for (const [requestId, entry] of this.pendingUiRequests) {
23146
+ if (entry.sessionId !== sessionId)
23147
+ continue;
23148
+ this.pendingUiRequests.delete(requestId);
23149
+ entry.resolve({ cancelled: true });
23150
+ }
23151
+ }
22940
23152
  /** M5 — per-connector live status from the running Query. Returns [] if the
22941
23153
  * session isn't resident or the SDK call throws (idle/closed). */
22942
23154
  async mcpServerStatus(sessionId) {
@@ -23086,6 +23298,135 @@ ${t}`)
23086
23298
  setConfig(projectDir) {
23087
23299
  this.projectDir = projectDir;
23088
23300
  }
23301
+ // ── Tab-driven pre-warming ──────────────────────────────────────────────────
23302
+ // Warming = spawn the cli.js process EARLY (the SDK spawns eagerly at query()) so its
23303
+ // ~1.6–3.4s node+cli.js boot overlaps idle time; the first user message is pushed only
23304
+ // when the user actually sends, by which point boot is done → first token is fast. The
23305
+ // warm process emits nothing (system/init is gated on the first push) so warming is
23306
+ // fire-and-forget and never worse than a cold start.
23307
+ /** Stable key over the CONSTRUCTION-immutable config a NEW conversation bakes into its
23308
+ * query(). A warm empty instance may be adopted only by a send whose key matches —
23309
+ * model/permissionMode/sources are reconciled live at adoption; `url` is excluded
23310
+ * (warmed with '' so browser navigation doesn't invalidate the standby). */
23311
+ warmKey(p) {
23312
+ const cwd = p.projectDir ?? this.projectDir ?? p.sources?.[0] ?? "";
23313
+ return JSON.stringify({
23314
+ cwd,
23315
+ effort: p.effort ?? "",
23316
+ mcp: Object.keys(p.mcpServers ?? {}).sort(),
23317
+ agents: p.agents ? Object.keys(p.agents).sort() : [],
23318
+ settings: p.settings ?? null,
23319
+ settingSources: [...p.settingSources ?? []].sort(),
23320
+ sandbox: p.sandbox ?? null,
23321
+ env: p.env ?? null,
23322
+ thinking: p.thinking ?? null,
23323
+ engineEnv: p.engineEnv ?? null,
23324
+ maxTurns: p.maxTurns ?? null,
23325
+ maxBudgetUsd: p.maxBudgetUsd ?? null,
23326
+ outputStyle: p.outputStyle ?? null
23327
+ });
23328
+ }
23329
+ /** Pre-warm a session (no user message pushed). Two kinds:
23330
+ * • RESUME (params.sessionId): the resumed session becomes resident, so a later
23331
+ * chat:send for it just pushes — no spawn/boot wait. Idempotent if already resident.
23332
+ * • EMPTY (no sessionId): the single standby new-session instance (warmEmpty), adopted
23333
+ * by the next matching NEW conversation. Replaced if the config key changed.
23334
+ * Best-effort: no open project ⇒ no-op; a spawn failure is swallowed (warm=true keeps
23335
+ * createLiveSession from emitting a client chat:error). */
23336
+ async warmSession(params) {
23337
+ const cwd = params.projectDir ?? this.projectDir ?? params.sources?.[0];
23338
+ if (!cwd)
23339
+ return;
23340
+ if (params.sessionId) {
23341
+ if (this.liveSessions.has(params.sessionId))
23342
+ return;
23343
+ } else {
23344
+ const key2 = this.warmKey(params);
23345
+ if (this.warmEmpty && this.warmEmptyKey === key2)
23346
+ return;
23347
+ if (this.warmEmpty)
23348
+ this.discardWarmEmpty();
23349
+ }
23350
+ let live;
23351
+ try {
23352
+ live = await this.createLiveSession({ ...params, message: "", clientRequestId: void 0, origin: void 0, url: "", warm: true });
23353
+ } catch (err) {
23354
+ log("warmSession failed (best-effort):", err instanceof Error ? err.message : String(err));
23355
+ return;
23356
+ }
23357
+ if (params.sessionId) {
23358
+ if (this.activeTabSession !== params.sessionId)
23359
+ this.scheduleIdleReap(live);
23360
+ } else {
23361
+ this.warmEmpty = live;
23362
+ this.warmEmptyKey = this.warmKey(params);
23363
+ }
23364
+ }
23365
+ /** A tab gained focus (sessionId set) or a NEW-conversation tab is focused (sessionId
23366
+ * omitted). Pins the focused session against idle reap and releases the previously
23367
+ * focused one so its 15-min idle clock starts ("time since last switched-to"). A
23368
+ * focused session that isn't resident (e.g. switched back to a reaped tab) is
23369
+ * warm-resumed so it's ready before the user types; a focused new-conversation tab
23370
+ * ensures the standby empty instance exists. */
23371
+ async setActiveSession(sessionId, params) {
23372
+ const prev = this.activeTabSession;
23373
+ if (prev && prev !== sessionId)
23374
+ this.setSessionKeepAlive(prev, false, "tab");
23375
+ this.activeTabSession = sessionId ?? null;
23376
+ if (sessionId) {
23377
+ if (!this.liveSessions.has(sessionId))
23378
+ await this.warmSession({ ...params, sessionId });
23379
+ this.setSessionKeepAlive(sessionId, true, "tab");
23380
+ } else {
23381
+ await this.warmSession({ ...params, sessionId: void 0 });
23382
+ }
23383
+ }
23384
+ /** Bind the standby empty instance to a brand-new conversation: set its routing/title,
23385
+ * reconcile the live-settable config the warm query couldn't bake, and re-seed a fresh
23386
+ * standby. The caller (sendMessage) then pushes the user message as usual. */
23387
+ adoptWarmEmpty(params) {
23388
+ const live = this.warmEmpty;
23389
+ this.warmEmpty = null;
23390
+ this.warmEmptyKey = null;
23391
+ live.clientRequestId = params.clientRequestId;
23392
+ live.origin = params.origin;
23393
+ if (params.browserId !== void 0)
23394
+ live.browserId = params.browserId;
23395
+ if (params.windowId !== void 0)
23396
+ live.windowId = params.windowId;
23397
+ live.pendingTitle = params.message.trim().slice(0, 80);
23398
+ if (params.model)
23399
+ void live.query.setModel(params.model || void 0).catch(() => {
23400
+ });
23401
+ if (params.permissionMode)
23402
+ void live.query.setPermissionMode(params.permissionMode).catch(() => {
23403
+ });
23404
+ const nextSources = params.sources ?? [];
23405
+ if (!sourcesEqual(live.sources, nextSources)) {
23406
+ void live.query.applyFlagSettings({ permissions: { additionalDirectories: nextSources } }).then(() => {
23407
+ live.sources = nextSources;
23408
+ }).catch((err) => log("adoptWarmEmpty applyFlagSettings failed:", err instanceof Error ? err.message : String(err)));
23409
+ }
23410
+ void this.warmSession({ ...params, sessionId: void 0, clientRequestId: void 0 });
23411
+ return live;
23412
+ }
23413
+ /** Tear down the standby empty instance (no sessionId ⇒ not in liveSessions / never
23414
+ * idle-reaped). Used when its config key goes stale or on dispose. */
23415
+ discardWarmEmpty() {
23416
+ const live = this.warmEmpty;
23417
+ this.warmEmpty = null;
23418
+ this.warmEmptyKey = null;
23419
+ if (!live || live.reaped)
23420
+ return;
23421
+ live.reaped = true;
23422
+ this.pendingLiveSessions.delete(live);
23423
+ try {
23424
+ live.abort.abort();
23425
+ } catch {
23426
+ }
23427
+ killProcessTree(live.child, "SIGKILL");
23428
+ live.pump.end();
23429
+ }
23089
23430
  buildSystemPrompt(url2) {
23090
23431
  const cli = `ELECTRON_RUN_AS_NODE=1 "${process.execPath}" "${this.browserCliPath}"`;
23091
23432
  const pageInfo = url2 ? `
@@ -23192,7 +23533,11 @@ CRITICAL RULES:
23192
23533
  return;
23193
23534
  }
23194
23535
  if (!live) {
23195
- live = await this.createLiveSession(params);
23536
+ if (!params.sessionId && this.warmEmpty && this.warmEmptyKey === this.warmKey(params)) {
23537
+ live = this.adoptWarmEmpty(params);
23538
+ } else {
23539
+ live = await this.createLiveSession(params);
23540
+ }
23196
23541
  } else {
23197
23542
  live.streaming = true;
23198
23543
  live.lastActivity = Date.now();
@@ -23387,11 +23732,33 @@ CRITICAL RULES:
23387
23732
  });
23388
23733
  mcp["tesseract-canvas"] = createSdkMcpServer({ name: "tesseract-canvas", version: "0.1.0", tools: canvasTools });
23389
23734
  }
23735
+ {
23736
+ const projectDir = this.projectDir;
23737
+ const reportTools = buildReportTools({
23738
+ emitReport: (report) => {
23739
+ const ts = Date.now();
23740
+ this.send({ type: "agent:report_ready", sessionId: live.sessionId, report, ts }, live.origin);
23741
+ void appendReportArtifact(live.sessionId, projectDir, report).catch(() => {
23742
+ });
23743
+ }
23744
+ });
23745
+ mcp["tesseract-report"] = createSdkMcpServer({ name: "tesseract-report", version: "0.1.0", tools: reportTools });
23746
+ }
23747
+ {
23748
+ const uiTools = buildInteractiveUiTools({
23749
+ raiseUiRequest: (spec) => this.raiseUiRequest(live, spec),
23750
+ emitProgress: (stages, note) => this.send({ type: "agent:mission_progress", sessionId: live.sessionId, stages, note, ts: Date.now() }, live.origin)
23751
+ });
23752
+ mcp["tesseract-interactive-ui"] = createSdkMcpServer({ name: "tesseract-interactive-ui", version: "0.1.0", tools: uiTools });
23753
+ }
23390
23754
  return Object.keys(mcp).length > 0 ? { mcpServers: mcp } : {};
23391
23755
  })(),
23392
23756
  // Skills (Epic H): a local plugin bundling the enabled ClawHub skills, so the SDK
23393
23757
  // discovers their SKILL.md files (isolated from ~/.claude + the edited project).
23394
- ...this.skillsPlugin ? { plugins: [this.skillsPlugin] } : {},
23758
+ ...(() => {
23759
+ const plugins = [this.skillsPlugin, params.missionSkillId ? this.missionSkillsPlugin : null].filter(Boolean);
23760
+ return plugins.length > 0 ? { plugins } : {};
23761
+ })(),
23395
23762
  ...params.maxTurns && params.maxTurns > 0 ? { maxTurns: params.maxTurns } : {},
23396
23763
  ...params.maxBudgetUsd && params.maxBudgetUsd > 0 ? { maxBudgetUsd: params.maxBudgetUsd } : {},
23397
23764
  // Surface every gated tool call to the client for approval. The SDK only
@@ -23528,7 +23895,8 @@ CRITICAL RULES:
23528
23895
  const nodeExe2 = resolveNodeExecutable();
23529
23896
  const exe = nodeExe2 ?? process.execPath;
23530
23897
  const runAsNode = exe === process.execPath && !!process.versions.electron;
23531
- const spawnEnv = { ...options.env, ...params.env ?? {}, ...params.engineEnv ?? {}, CCB_IPC_PORT: String(live.ipcPort), ...runAsNode ? { ELECTRON_RUN_AS_NODE: "1" } : {} };
23898
+ const compileCache = process.env.NODE_COMPILE_CACHE || join9(process.env.CCB_TESSERACT_DIR ?? join9(homedir4(), ".tesseract"), "node-compile-cache");
23899
+ const spawnEnv = { NODE_COMPILE_CACHE: compileCache, ...options.env, ...params.env ?? {}, ...params.engineEnv ?? {}, CCB_IPC_PORT: String(live.ipcPort), ...runAsNode ? { ELECTRON_RUN_AS_NODE: "1" } : {} };
23532
23900
  const child = spawn2(exe, [cliJs, ...options.args], {
23533
23901
  cwd: options.cwd,
23534
23902
  env: spawnEnv,
@@ -23547,7 +23915,9 @@ CRITICAL RULES:
23547
23915
  // restores the SDK's dynamic command list, gitStatus/date context,
23548
23916
  // env-info and memory/CLAUDE.md preamble that a bare custom string
23549
23917
  // prompt suppresses — while `append` keeps the Tesseract browser tools.
23550
- systemPrompt: { type: "preset", preset: "claude_code", append: this.buildSystemPrompt(params.url) },
23918
+ systemPrompt: { type: "preset", preset: "claude_code", append: this.buildSystemPrompt(params.url) + (params.appendSystemPrompt ? `
23919
+
23920
+ ${params.appendSystemPrompt}` : "") },
23551
23921
  includePartialMessages: true,
23552
23922
  // Opt into AskUserQuestion option previews. Without a previewFormat the
23553
23923
  // SDK omits the preview-field guidance from the tool prompt, so the
@@ -23585,10 +23955,12 @@ CRITICAL RULES:
23585
23955
  live.ipcServer?.close();
23586
23956
  } catch {
23587
23957
  }
23588
- const sid = live.sessionId || "";
23589
- const crid = live.sessionId ? void 0 : live.clientRequestId;
23590
- this.send({ type: "chat:error", sessionId: sid, clientRequestId: crid, error: err instanceof Error ? err.message : "Failed to start the agent session." }, params.origin);
23591
- this.send({ type: "agent:status", sessionId: sid, clientRequestId: crid, status: "error" }, params.origin);
23958
+ if (!params.warm) {
23959
+ const sid = live.sessionId || "";
23960
+ const crid = live.sessionId ? void 0 : live.clientRequestId;
23961
+ this.send({ type: "chat:error", sessionId: sid, clientRequestId: crid, error: err instanceof Error ? err.message : "Failed to start the agent session." }, params.origin);
23962
+ this.send({ type: "agent:status", sessionId: sid, clientRequestId: crid, status: "error" }, params.origin);
23963
+ }
23592
23964
  throw err;
23593
23965
  }
23594
23966
  if (params.sessionId)
@@ -24038,6 +24410,8 @@ ${err.stack}` : ""}` : String(err);
24038
24410
  live.steerFlushTimer = void 0;
24039
24411
  }
24040
24412
  this.clearStopWatchdog(live);
24413
+ if (live.sessionId)
24414
+ this.cancelSessionUiRequests(live.sessionId);
24041
24415
  if (live.sessionId)
24042
24416
  this.liveSessions.delete(live.sessionId);
24043
24417
  this.pendingLiveSessions.delete(live);
@@ -24298,6 +24672,9 @@ ${err.stack}` : ""}` : String(err);
24298
24672
  this.cronToolsDeps = null;
24299
24673
  this.cronTaskSessions.clear();
24300
24674
  this.cronRequestTask.clear();
24675
+ this.warmEmpty = null;
24676
+ this.warmEmptyKey = null;
24677
+ this.activeTabSession = null;
24301
24678
  for (const live of [...this.liveSessions.values(), ...this.pendingLiveSessions]) {
24302
24679
  if (live.reaped)
24303
24680
  continue;
@@ -24311,6 +24688,8 @@ ${err.stack}` : ""}` : String(err);
24311
24688
  live.steerFlushTimer = void 0;
24312
24689
  }
24313
24690
  this.clearStopWatchdog(live);
24691
+ if (live.sessionId)
24692
+ this.cancelSessionUiRequests(live.sessionId);
24314
24693
  if (live.sessionId)
24315
24694
  this.liveSessions.delete(live.sessionId);
24316
24695
  this.pendingLiveSessions.delete(live);
@@ -24324,6 +24703,8 @@ ${err.stack}` : ""}` : String(err);
24324
24703
  }
24325
24704
  /** Memory-pressure entrypoint: moderate trims long-idle sessions; critical reaps all idle. */
24326
24705
  reclaim(level) {
24706
+ if (level === "critical")
24707
+ this.discardWarmEmpty();
24327
24708
  const n = level === "critical" ? this.reapAllIdle() : this.reapIdleSessions(MODERATE_IDLE_MS);
24328
24709
  if (n > 0)
24329
24710
  log(`Memory reclaim (${level}): reaped ${n} idle session(s)`);
@@ -42755,7 +43136,7 @@ var require_selfsigned = __commonJS({
42755
43136
  import tls from "node:tls";
42756
43137
  import { X509Certificate, timingSafeEqual } from "node:crypto";
42757
43138
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync10, mkdirSync as mkdirSync8 } from "node:fs";
42758
- import { dirname as dirname10 } from "node:path";
43139
+ import { dirname as dirname9 } from "node:path";
42759
43140
  function fingerprintOf(certPem) {
42760
43141
  return new X509Certificate(certPem).fingerprint256;
42761
43142
  }
@@ -42775,7 +43156,7 @@ async function loadOrCreateIdentity(certPath, keyPath) {
42775
43156
  const tenYears = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3);
42776
43157
  const pems = await (0, import_selfsigned.generate)([{ name: "commonName", value: "tesseract-peer" }], { keySize: 2048, notAfterDate: tenYears, algorithm: "sha256" });
42777
43158
  try {
42778
- mkdirSync8(dirname10(certPath), { recursive: true });
43159
+ mkdirSync8(dirname9(certPath), { recursive: true });
42779
43160
  } catch {
42780
43161
  }
42781
43162
  try {
@@ -44301,12 +44682,12 @@ import { createHash as createHash5 } from "node:crypto";
44301
44682
  import { spawn as spawn8 } from "node:child_process";
44302
44683
  import { fileURLToPath as fileURLToPath2 } from "node:url";
44303
44684
  import { homedir as homedir16 } from "node:os";
44304
- import { dirname as dirname18, join as join38, resolve as resolve9 } from "node:path";
44685
+ import { dirname as dirname17, join as join38, resolve as resolve9 } from "node:path";
44305
44686
  import { createInterface } from "node:readline/promises";
44306
44687
 
44307
44688
  // ../../packages/host-core/dist/control-link.js
44308
44689
  import { hostname as hostname3 } from "node:os";
44309
- import { join as join30, basename as basename7 } from "node:path";
44690
+ import { join as join30, basename as basename6 } from "node:path";
44310
44691
  import { mkdirSync as mkdirSync11 } from "node:fs";
44311
44692
 
44312
44693
  // ../../node_modules/ws/wrapper.mjs
@@ -44733,7 +45114,7 @@ import { join as join18 } from "node:path";
44733
45114
 
44734
45115
  // ../../packages/host-core/dist/agent-backend.js
44735
45116
  async function createClaudeCodeBackend(send2, onBrowserRequest, loadPluginRuntime2) {
44736
- const { AgentManager: AgentManager2, SessionStore: SessionStore2, resolveSessionFile: resolveSessionFile2, readSessionArtifacts: readSessionArtifacts2 } = await Promise.resolve().then(() => (init_dist4(), dist_exports));
45117
+ const { AgentManager: AgentManager2, SessionStore: SessionStore2, readSessionArtifacts: readSessionArtifacts2 } = await Promise.resolve().then(() => (init_dist4(), dist_exports));
44737
45118
  const manager2 = new AgentManager2(send2, onBrowserRequest);
44738
45119
  const store = new SessionStore2();
44739
45120
  const refreshPlugins = async (projectDir) => {
@@ -44761,8 +45142,6 @@ async function createClaudeCodeBackend(send2, onBrowserRequest, loadPluginRuntim
44761
45142
  getMessages: (sessionId) => store.getSessionMessages(sessionId),
44762
45143
  getSessionArtifacts: (sessionId, projectDir) => readSessionArtifacts2(sessionId, projectDir),
44763
45144
  summarizeSession: async (sessionId) => manager2.summarizeSession(await store.getSessionMessages(sessionId)),
44764
- isSessionBusy: (sessionId) => manager2.isSessionBusy(sessionId),
44765
- resolveSessionFile: (sessionId, projectDir) => resolveSessionFile2(sessionId, projectDir),
44766
45145
  setConfig: (projectDir) => {
44767
45146
  manager2.setConfig(projectDir);
44768
45147
  void refreshPlugins(projectDir);
@@ -44773,9 +45152,12 @@ async function createClaudeCodeBackend(send2, onBrowserRequest, loadPluginRuntim
44773
45152
  setImageGen: (runner) => manager2.setImageGen(runner),
44774
45153
  setRunMode: (sessionId, opts) => manager2.setRunMode(sessionId, opts),
44775
45154
  setSessionKeepAlive: (sessionId, keep, holder) => manager2.setSessionKeepAlive(sessionId, keep, holder),
45155
+ warmSession: (params) => manager2.warmSession(params),
45156
+ setActiveSession: (sessionId, params) => manager2.setActiveSession(sessionId, params),
44776
45157
  clearSessionOrigin: (sessionId) => manager2.clearSessionOrigin(sessionId),
44777
45158
  stopTask: (sessionId, taskId, callerOrigin) => manager2.stopTask(sessionId, taskId, callerOrigin),
44778
45159
  respondToElicitation: (requestId, decision, callerOrigin) => manager2.respondToElicitation(requestId, decision, callerOrigin),
45160
+ respondToUiRequest: (requestId, response, callerOrigin) => manager2.respondToUiRequest(requestId, response, callerOrigin),
44779
45161
  mcpServerStatus: (sessionId) => manager2.mcpServerStatus(sessionId),
44780
45162
  reconnectMcpServer: (sessionId, name) => manager2.reconnectMcpServer(sessionId, name),
44781
45163
  toggleMcpServer: (sessionId, name, enabled) => manager2.toggleMcpServer(sessionId, name, enabled),
@@ -45361,8 +45743,8 @@ function createOpenClawAdapter(pluginId, config2, secretResolver, registry2) {
45361
45743
  pluginId,
45362
45744
  config: config2,
45363
45745
  secret: (envKey2) => secretResolver(pluginId, envKey2),
45364
- registerTool: (tool5) => {
45365
- const got = typeof tool5 === "function" ? tool5({}) : tool5;
45746
+ registerTool: (tool7) => {
45747
+ const got = typeof tool7 === "function" ? tool7({}) : tool7;
45366
45748
  const arr = Array.isArray(got) ? got : got ? [got] : [];
45367
45749
  for (const t of arr) {
45368
45750
  const c = convertTool(t);
@@ -45431,10 +45813,10 @@ var HostPluginApi = class {
45431
45813
  secret(envKey2) {
45432
45814
  return this.secretResolver(this.pluginId, envKey2);
45433
45815
  }
45434
- registerTool(tool5) {
45816
+ registerTool(tool7) {
45435
45817
  const prefix = this.pluginId.replace(/[^a-zA-Z0-9_]/g, "_");
45436
- const name = tool5.name.startsWith(`${prefix}_`) ? tool5.name : `${prefix}_${tool5.name}`;
45437
- this.registry.tools.push({ ...tool5, name });
45818
+ const name = tool7.name.startsWith(`${prefix}_`) ? tool7.name : `${prefix}_${tool7.name}`;
45819
+ this.registry.tools.push({ ...tool7, name });
45438
45820
  }
45439
45821
  registerHook(reg) {
45440
45822
  this.registry.hooks.push(reg);
@@ -46061,9 +46443,9 @@ function augmentedPath() {
46061
46443
  return parts.join(sep8);
46062
46444
  }
46063
46445
  function which(bin, path5) {
46064
- const tool5 = process.platform === "win32" ? "where" : "which";
46446
+ const tool7 = process.platform === "win32" ? "where" : "which";
46065
46447
  try {
46066
- const out = execFileSync(tool5, [bin], { env: { ...process.env, PATH: path5 }, timeout: 4e3, encoding: "utf-8" });
46448
+ const out = execFileSync(tool7, [bin], { env: { ...process.env, PATH: path5 }, timeout: 4e3, encoding: "utf-8" });
46067
46449
  return out.split(/\r?\n/).map((l) => l.trim()).find(Boolean) || void 0;
46068
46450
  } catch {
46069
46451
  return void 0;
@@ -46179,12 +46561,12 @@ async function loadPluginRuntime(opts) {
46179
46561
  function buildDaemonPluginLoader(pluginsDir) {
46180
46562
  return async () => {
46181
46563
  const skillsPlugin = daemonSkillsPluginConfig();
46182
- const scan2 = join16(pluginsDir, "node_modules");
46183
- if (!existsSync7(scan2))
46564
+ const scan = join16(pluginsDir, "node_modules");
46565
+ if (!existsSync7(scan))
46184
46566
  return { tools: [], hooks: [], engines: [], skillsPlugin };
46185
46567
  writeOpenClawShim(pluginsDir);
46186
46568
  const rt = await loadPluginRuntime({
46187
- roots: [{ dir: scan2, trusted: true, relaxHardlink: true }],
46569
+ roots: [{ dir: scan, trusted: true, relaxHardlink: true }],
46188
46570
  secretResolver: (_pluginId, envKey2) => process.env[envKey2]
46189
46571
  });
46190
46572
  return { ...rt, skillsPlugin };
@@ -46196,7 +46578,7 @@ function daemonSkillsPluginConfig() {
46196
46578
  return null;
46197
46579
  const enabled = join16(root, "skills");
46198
46580
  try {
46199
- const hasSkill = readdirSync(enabled).some((slug) => existsSync7(join16(enabled, slug, "SKILL.md")));
46581
+ const hasSkill = readdirSync(enabled).some((slug2) => existsSync7(join16(enabled, slug2, "SKILL.md")));
46200
46582
  return hasSkill ? { type: "local", path: root } : null;
46201
46583
  } catch {
46202
46584
  return null;
@@ -47556,125 +47938,6 @@ function unwatchAllRemote() {
47556
47938
  remoteWatches.clear();
47557
47939
  }
47558
47940
 
47559
- // ../../packages/host-core/dist/session-watch.js
47560
- import { watch as watch2 } from "node:fs";
47561
- import { dirname as dirname8, basename as basename3 } from "node:path";
47562
- var DEBOUNCE_MS = 120;
47563
- var BUSY_GRACE_MS = 2e3;
47564
- var watches = /* @__PURE__ */ new Map();
47565
- var sessionToFile = /* @__PURE__ */ new Map();
47566
- async function watchSession(sessionId, key2, projectDir, deps) {
47567
- let filePath = sessionToFile.get(sessionId);
47568
- if (!filePath) {
47569
- const resolved = await deps.resolveFile?.(sessionId, projectDir);
47570
- if (!resolved)
47571
- return;
47572
- filePath = resolved;
47573
- sessionToFile.set(sessionId, filePath);
47574
- }
47575
- const baseline = await deps.getMessages(sessionId).then((m) => m.length).catch(() => 0);
47576
- const existing = watches.get(filePath);
47577
- if (existing) {
47578
- existing.listeners.set(key2, { emittedCount: baseline, deps });
47579
- return;
47580
- }
47581
- const fileName = basename3(filePath);
47582
- const fw = {
47583
- watcher: void 0,
47584
- sessionId,
47585
- listeners: /* @__PURE__ */ new Map([[key2, { emittedCount: baseline, deps }]]),
47586
- timer: null,
47587
- lastBusyAt: 0
47588
- };
47589
- try {
47590
- fw.watcher = watch2(dirname8(filePath), (_event, changed) => {
47591
- if (changed && basename3(String(changed)) !== fileName)
47592
- return;
47593
- scheduleScan(filePath);
47594
- });
47595
- } catch {
47596
- return;
47597
- }
47598
- watches.set(filePath, fw);
47599
- }
47600
- function unwatchSession(sessionId, key2) {
47601
- const filePath = sessionToFile.get(sessionId);
47602
- if (!filePath)
47603
- return;
47604
- const fw = watches.get(filePath);
47605
- if (!fw)
47606
- return;
47607
- fw.listeners.delete(key2);
47608
- if (fw.listeners.size === 0)
47609
- closeWatch(filePath);
47610
- }
47611
- function unwatchSessionClient(key2) {
47612
- for (const [filePath, fw] of [...watches]) {
47613
- if (fw.listeners.delete(key2) && fw.listeners.size === 0)
47614
- closeWatch(filePath);
47615
- }
47616
- }
47617
- function unwatchAllSessions() {
47618
- for (const [filePath] of [...watches])
47619
- closeWatch(filePath);
47620
- }
47621
- function closeWatch(filePath) {
47622
- const fw = watches.get(filePath);
47623
- if (!fw)
47624
- return;
47625
- if (fw.timer)
47626
- clearTimeout(fw.timer);
47627
- try {
47628
- fw.watcher.close();
47629
- } catch {
47630
- }
47631
- watches.delete(filePath);
47632
- sessionToFile.delete(fw.sessionId);
47633
- }
47634
- function scheduleScan(filePath) {
47635
- const fw = watches.get(filePath);
47636
- if (!fw)
47637
- return;
47638
- if (fw.timer)
47639
- clearTimeout(fw.timer);
47640
- fw.timer = setTimeout(() => {
47641
- fw.timer = null;
47642
- void scan(filePath);
47643
- }, DEBOUNCE_MS);
47644
- }
47645
- async function scan(filePath) {
47646
- const fw = watches.get(filePath);
47647
- if (!fw)
47648
- return;
47649
- const { sessionId } = fw;
47650
- const any2 = fw.listeners.values().next().value;
47651
- if (!any2)
47652
- return;
47653
- const busy = any2.deps.isSessionBusy?.(sessionId) === true;
47654
- const now = Date.now();
47655
- if (busy)
47656
- fw.lastBusyAt = now;
47657
- const inGrace = busy || now - fw.lastBusyAt < BUSY_GRACE_MS;
47658
- const messages = await any2.deps.getMessages(sessionId).catch(() => null);
47659
- if (!messages)
47660
- return;
47661
- const len = messages.length;
47662
- const willCompact = !inGrace && [...fw.listeners.values()].some((l) => len < l.emittedCount);
47663
- const artifacts = willCompact ? await (any2.deps.getSessionArtifacts?.(sessionId).catch(() => []) ?? Promise.resolve([])) : [];
47664
- for (const [key2, listener] of fw.listeners) {
47665
- if (inGrace) {
47666
- listener.emittedCount = len;
47667
- continue;
47668
- }
47669
- if (len < listener.emittedCount) {
47670
- listener.deps.emit(key2, { type: "session:messages", sessionId, messages, artifacts });
47671
- } else if (len > listener.emittedCount) {
47672
- listener.deps.emit(key2, { type: "session:appended", sessionId, messages: messages.slice(listener.emittedCount) });
47673
- }
47674
- listener.emittedCount = len;
47675
- }
47676
- }
47677
-
47678
47941
  // ../../packages/host-core/dist/fs-datachannel-host.js
47679
47942
  var SCTP_BUF = 16 * 1024 * 1024;
47680
47943
  var ndcPromise = null;
@@ -47887,7 +48150,7 @@ function dynamicMetrics() {
47887
48150
  // ../../packages/host-core/dist/fs-transfer.js
47888
48151
  import { readdir as readdir4, lstat as lstat2, mkdir as mkdir6, chmod, rm as rm2, readlink, symlink, stat as stat3 } from "node:fs/promises";
47889
48152
  import { openSync as openSync3, readSync as readSync2, closeSync as closeSync3, existsSync as existsSync9, createWriteStream } from "node:fs";
47890
- import { dirname as dirname9, join as join22, relative as relative6, sep as sep6 } from "node:path";
48153
+ import { dirname as dirname8, join as join22, relative as relative6, sep as sep6 } from "node:path";
47891
48154
 
47892
48155
  // ../../packages/host-core/dist/active-link/paths.js
47893
48156
  import { homedir as homedir11 } from "node:os";
@@ -48234,9 +48497,9 @@ async function writeInboundChunk(msg, send2) {
48234
48497
  try {
48235
48498
  if (msg.kind === "symlink") {
48236
48499
  const target = Buffer.from(msg.base64, "base64").toString("utf-8");
48237
- const resolved = join22(dirname9(abs), target);
48500
+ const resolved = join22(dirname8(abs), target);
48238
48501
  if (isInsideRoot(resolved, s.destRoot)) {
48239
- await mkdir6(dirname9(abs), { recursive: true }).catch(() => {
48502
+ await mkdir6(dirname8(abs), { recursive: true }).catch(() => {
48240
48503
  });
48241
48504
  await symlink(target, abs).catch(() => {
48242
48505
  });
@@ -48249,7 +48512,7 @@ async function writeInboundChunk(msg, send2) {
48249
48512
  if (s.curIx !== msg.fileIx || !s.curStream) {
48250
48513
  if (s.curStream)
48251
48514
  await new Promise((r) => s.curStream.end(r));
48252
- await mkdir6(dirname9(abs), { recursive: true }).catch(() => {
48515
+ await mkdir6(dirname8(abs), { recursive: true }).catch(() => {
48253
48516
  });
48254
48517
  s.curStream = createWriteStream(abs);
48255
48518
  s.curIx = msg.fileIx;
@@ -48766,13 +49029,13 @@ async function openPeerLink(peerHostId, ctx) {
48766
49029
  // ../../packages/host-core/dist/active-link/engine.js
48767
49030
  import { lstat as lstat7, readlink as readlink4, rm as rm3, mkdir as mkdir8, symlink as symlink2, rename as rename2, utimes as utimes2, writeFile as writeFile6 } from "node:fs/promises";
48768
49031
  import { existsSync as existsSync12 } from "node:fs";
48769
- import { dirname as dirname14, join as join28, resolve as resolve8 } from "node:path";
49032
+ import { dirname as dirname13, join as join28, resolve as resolve8 } from "node:path";
48770
49033
 
48771
49034
  // ../../packages/host-core/dist/active-link/blocks.js
48772
49035
  import { createHash as createHash3 } from "node:crypto";
48773
49036
  import { createReadStream } from "node:fs";
48774
49037
  import { open, mkdir as mkdir7, rename, chmod as chmod2, utimes, unlink } from "node:fs/promises";
48775
- import { dirname as dirname11 } from "node:path";
49038
+ import { dirname as dirname10 } from "node:path";
48776
49039
  var CHUNK3 = 1024 * 1024;
48777
49040
  var MAX_INFLIGHT2 = 8 * CHUNK3;
48778
49041
  var IDLE_MS2 = 6e4;
@@ -48900,7 +49163,7 @@ var StagingWriter = class {
48900
49163
  }
48901
49164
  async ensureOpen() {
48902
49165
  if (!this.fh) {
48903
- await mkdir7(dirname11(this.stagingPath), { recursive: true });
49166
+ await mkdir7(dirname10(this.stagingPath), { recursive: true });
48904
49167
  this.fh = await open(this.stagingPath, "w");
48905
49168
  }
48906
49169
  return this.fh;
@@ -48930,7 +49193,7 @@ var StagingWriter = class {
48930
49193
  });
48931
49194
  await fh.close();
48932
49195
  this.fh = null;
48933
- await mkdir7(dirname11(dest), { recursive: true });
49196
+ await mkdir7(dirname10(dest), { recursive: true });
48934
49197
  await chmod2(this.stagingPath, mode & 511).catch(() => {
48935
49198
  });
48936
49199
  const secs = mtimeMs / 1e3;
@@ -48963,7 +49226,7 @@ var BlockAssembler = class {
48963
49226
  this.stagingPath = stagingPath;
48964
49227
  }
48965
49228
  async open() {
48966
- await mkdir7(dirname11(this.stagingPath), { recursive: true });
49229
+ await mkdir7(dirname10(this.stagingPath), { recursive: true });
48967
49230
  this.fh = await open(this.stagingPath, "w");
48968
49231
  }
48969
49232
  /** Write a received block at its byte offset (index * CHUNK). */
@@ -49007,7 +49270,7 @@ var BlockAssembler = class {
49007
49270
  });
49008
49271
  return false;
49009
49272
  }
49010
- await mkdir7(dirname11(dest), { recursive: true });
49273
+ await mkdir7(dirname10(dest), { recursive: true });
49011
49274
  await chmod2(this.stagingPath, mode & 511).catch(() => {
49012
49275
  });
49013
49276
  const secs = mtimeMs / 1e3;
@@ -49593,10 +49856,10 @@ var ReaddirpStream = class extends Readable {
49593
49856
  }
49594
49857
  async _formatEntry(dirent, path5) {
49595
49858
  let entry;
49596
- const basename8 = this._isDirent ? dirent.name : dirent;
49859
+ const basename7 = this._isDirent ? dirent.name : dirent;
49597
49860
  try {
49598
- const fullPath = presolve(pjoin(path5, basename8));
49599
- entry = { path: prelative(this._root, fullPath), fullPath, basename: basename8 };
49861
+ const fullPath = presolve(pjoin(path5, basename7));
49862
+ entry = { path: prelative(this._root, fullPath), fullPath, basename: basename7 };
49600
49863
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
49601
49864
  } catch (err) {
49602
49865
  this._onError(err);
@@ -50135,9 +50398,9 @@ var NodeFsHandler = class {
50135
50398
  _watchWithNodeFs(path5, listener) {
50136
50399
  const opts = this.fsw.options;
50137
50400
  const directory = sysPath.dirname(path5);
50138
- const basename8 = sysPath.basename(path5);
50401
+ const basename7 = sysPath.basename(path5);
50139
50402
  const parent = this.fsw._getWatchedDir(directory);
50140
- parent.add(basename8);
50403
+ parent.add(basename7);
50141
50404
  const absolutePath = sysPath.resolve(path5);
50142
50405
  const options = {
50143
50406
  persistent: opts.persistent
@@ -50147,7 +50410,7 @@ var NodeFsHandler = class {
50147
50410
  let closer;
50148
50411
  if (opts.usePolling) {
50149
50412
  const enableBin = opts.interval !== opts.binaryInterval;
50150
- options.interval = enableBin && isBinaryPath(basename8) ? opts.binaryInterval : opts.interval;
50413
+ options.interval = enableBin && isBinaryPath(basename7) ? opts.binaryInterval : opts.interval;
50151
50414
  closer = setFsWatchFileListener(path5, absolutePath, options, {
50152
50415
  listener,
50153
50416
  rawEmitter: this.fsw._emitRaw
@@ -50169,11 +50432,11 @@ var NodeFsHandler = class {
50169
50432
  if (this.fsw.closed) {
50170
50433
  return;
50171
50434
  }
50172
- const dirname19 = sysPath.dirname(file2);
50173
- const basename8 = sysPath.basename(file2);
50174
- const parent = this.fsw._getWatchedDir(dirname19);
50435
+ const dirname18 = sysPath.dirname(file2);
50436
+ const basename7 = sysPath.basename(file2);
50437
+ const parent = this.fsw._getWatchedDir(dirname18);
50175
50438
  let prevStats = stats;
50176
- if (parent.has(basename8))
50439
+ if (parent.has(basename7))
50177
50440
  return;
50178
50441
  const listener = async (path5, newStats) => {
50179
50442
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file2, 5))
@@ -50198,9 +50461,9 @@ var NodeFsHandler = class {
50198
50461
  prevStats = newStats2;
50199
50462
  }
50200
50463
  } catch (error51) {
50201
- this.fsw._remove(dirname19, basename8);
50464
+ this.fsw._remove(dirname18, basename7);
50202
50465
  }
50203
- } else if (parent.has(basename8)) {
50466
+ } else if (parent.has(basename7)) {
50204
50467
  const at = newStats.atimeMs;
50205
50468
  const mt = newStats.mtimeMs;
50206
50469
  if (!at || at <= mt || mt !== prevStats.mtimeMs) {
@@ -51124,7 +51387,7 @@ var FSWatcher = class extends EventEmitter2 {
51124
51387
  return stream;
51125
51388
  }
51126
51389
  };
51127
- function watch3(paths, options = {}) {
51390
+ function watch2(paths, options = {}) {
51128
51391
  const watcher = new FSWatcher(options);
51129
51392
  watcher.add(paths);
51130
51393
  return watcher;
@@ -51142,7 +51405,7 @@ var LinkWatcher = class {
51142
51405
  this.debounceMs = opts.debounceMs ?? 500;
51143
51406
  }
51144
51407
  async start() {
51145
- this.fsw = watch3(this.opts.root, {
51408
+ this.fsw = watch2(this.opts.root, {
51146
51409
  ignoreInitial: true,
51147
51410
  // the engine does its own hashing scan on start
51148
51411
  followSymlinks: false,
@@ -51643,13 +51906,13 @@ var ActiveLinkEngine = class {
51643
51906
  * file so the path still exists — recorded as a FILE matching disk so the local
51644
51907
  * watcher doesn't churn. The target is containment-checked, never dereferenced. */
51645
51908
  async applySymlink(rel, dest, target, mtimeMs, mode) {
51646
- const resolved = resolve8(dirname14(dest), target);
51909
+ const resolved = resolve8(dirname13(dest), target);
51647
51910
  if (!isInsideRoot(resolved, this.opts.localRoot))
51648
51911
  return;
51649
51912
  const hash2 = hashString(target);
51650
51913
  const secs = mtimeMs / 1e3;
51651
51914
  try {
51652
- await mkdir8(dirname14(dest), { recursive: true });
51915
+ await mkdir8(dirname13(dest), { recursive: true });
51653
51916
  await rm3(dest, { force: true, recursive: true });
51654
51917
  try {
51655
51918
  await symlink2(target, dest);
@@ -51701,7 +51964,7 @@ var ActiveLinkEngine = class {
51701
51964
  return;
51702
51965
  if (fromRec && fromRec.kind === "file") {
51703
51966
  try {
51704
- await mkdir8(dirname14(toAbs2), { recursive: true });
51967
+ await mkdir8(dirname13(toAbs2), { recursive: true });
51705
51968
  await rename2(fromAbs, toAbs2);
51706
51969
  const secs = mtimeMs / 1e3;
51707
51970
  await utimes2(toAbs2, secs, secs).catch(() => {
@@ -51901,7 +52164,7 @@ var ActiveLinkEngine = class {
51901
52164
  };
51902
52165
 
51903
52166
  // ../../packages/host-core/dist/active-link/config.js
51904
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync11, mkdirSync as mkdirSync10, existsSync as existsSync13, renameSync as renameSync6, watch as watch4 } from "node:fs";
52167
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync11, mkdirSync as mkdirSync10, existsSync as existsSync13, renameSync as renameSync6, watch as watch3 } from "node:fs";
51905
52168
  import { join as join29 } from "node:path";
51906
52169
  function configPath2() {
51907
52170
  return join29(activeLinkRoot(), "links.json");
@@ -51975,7 +52238,7 @@ function watchLinkConfigs(onChange) {
51975
52238
  let timer = null;
51976
52239
  let watcher = null;
51977
52240
  try {
51978
- watcher = watch4(dir, (_evt, file2) => {
52241
+ watcher = watch3(dir, (_evt, file2) => {
51979
52242
  if (file2 && file2 !== "links.json")
51980
52243
  return;
51981
52244
  if (timer)
@@ -52384,7 +52647,7 @@ function clampDim(v, dflt) {
52384
52647
  init_dist2();
52385
52648
 
52386
52649
  // ../../packages/host-core/dist/voice-control.js
52387
- import { basename as basename6 } from "node:path";
52650
+ import { basename as basename5 } from "node:path";
52388
52651
 
52389
52652
  // ../../packages/voice-core/dist/providers/tool-adapters.js
52390
52653
  function toELClientTool(t) {
@@ -53707,7 +53970,7 @@ var VoiceControl = class {
53707
53970
  voice: msg.voice,
53708
53971
  language: msg.language,
53709
53972
  turnSensitivity: msg.turnSensitivity,
53710
- instructions: buildGuidePrompt({ projectName: projectDir ? basename6(projectDir) : void 0, sessions: msg.sessions }),
53973
+ instructions: buildGuidePrompt({ projectName: projectDir ? basename5(projectDir) : void 0, sessions: msg.sessions }),
53711
53974
  greeting: DEFAULT_GREETING
53712
53975
  };
53713
53976
  const orchestrator = new VoiceGuideOrchestrator({
@@ -54081,7 +54344,7 @@ function sendActiveLinkProvision(cfg2) {
54081
54344
  type: "provision",
54082
54345
  linkId: cfg2.linkId,
54083
54346
  fromHost: ownHostId(),
54084
- projectName: cfg2.remoteSubfolder ?? basename7(cfg2.localRoot),
54347
+ projectName: cfg2.remoteSubfolder ?? basename6(cfg2.localRoot),
54085
54348
  ...cfg2.remoteParent ? { remoteParent: cfg2.remoteParent } : {},
54086
54349
  ...cfg2.remoteSubfolder ? { remoteSubfolder: cfg2.remoteSubfolder } : {},
54087
54350
  ...cfg2.workspaceLinkId ? { workspaceLinkId: cfg2.workspaceLinkId } : {}
@@ -54288,8 +54551,11 @@ async function handleInboundAgent(msg, driverClientId) {
54288
54551
  ...msg.permissionMode ? { permissionMode: msg.permissionMode } : {},
54289
54552
  ...msg.effort ? { effort: msg.effort } : {},
54290
54553
  ...msg.model ? { model: msg.model } : {},
54291
- ...msg.jsonSchema ? { jsonSchema: msg.jsonSchema } : {}
54554
+ ...msg.jsonSchema ? { jsonSchema: msg.jsonSchema } : {},
54292
54555
  // M7 structured output
54556
+ ...msg.appendSystemPrompt ? { appendSystemPrompt: msg.appendSystemPrompt } : {},
54557
+ // mission hidden launch
54558
+ ...msg.missionSkillId ? { missionSkillId: msg.missionSkillId } : {}
54293
54559
  })).catch(() => {
54294
54560
  });
54295
54561
  break;
@@ -54316,21 +54582,6 @@ async function handleInboundAgent(msg, driverClientId) {
54316
54582
  send("agent", { type: "session:summary", clientRequestId: msg.clientRequestId, ...res }, route2);
54317
54583
  break;
54318
54584
  }
54319
- case "session:watch":
54320
- if (driverClientId) {
54321
- void watchSession(msg.sessionId, driverClientId, msg.projectDir, {
54322
- getMessages: (sid) => b.getMessages(sid),
54323
- isSessionBusy: (sid) => b.isSessionBusy?.(sid) ?? false,
54324
- resolveFile: (sid, dir) => b.resolveSessionFile?.(sid, dir) ?? Promise.resolve(null),
54325
- getSessionArtifacts: (sid) => b.getSessionArtifacts?.(sid) ?? Promise.resolve([]),
54326
- emit: (key2, m) => send("agent", m, { clientId: key2 })
54327
- });
54328
- }
54329
- break;
54330
- case "session:unwatch":
54331
- if (driverClientId)
54332
- unwatchSession(msg.sessionId, driverClientId);
54333
- break;
54334
54585
  case "session:fork": {
54335
54586
  const newId = await b.forkSession?.(msg.sessionId, { projectDir: msg.projectDir, upToMessageId: msg.upToMessageId, title: msg.title }).catch(() => null);
54336
54587
  if (newId)
@@ -54378,6 +54629,9 @@ async function handleInboundAgent(msg, driverClientId) {
54378
54629
  case "chat:elicitation":
54379
54630
  b.respondToElicitation?.(msg.requestId, { action: msg.action, content: msg.content }, origin);
54380
54631
  break;
54632
+ case "chat:ui_response":
54633
+ b.respondToUiRequest?.(msg.requestId, msg.response, origin);
54634
+ break;
54381
54635
  case "agent:mcp_reconnect":
54382
54636
  void b.reconnectMcpServer?.(msg.sessionId, msg.name);
54383
54637
  break;
@@ -54466,7 +54720,6 @@ function releaseDriver(clientId) {
54466
54720
  sealChains.delete(clientId);
54467
54721
  voiceControls.get(clientId)?.stop("disconnected");
54468
54722
  voiceControls.delete(clientId);
54469
- unwatchSessionClient(clientId);
54470
54723
  releaseClientPtys(clientId);
54471
54724
  releaseDcClient(clientId);
54472
54725
  for (const [sid, cid] of [...driverBySession]) {
@@ -54506,7 +54759,6 @@ function teardownHostRole() {
54506
54759
  driverBySession.clear();
54507
54760
  driverByRequest.clear();
54508
54761
  unwatchAllRemote();
54509
- unwatchAllSessions();
54510
54762
  releaseRemotePtys();
54511
54763
  try {
54512
54764
  hostBackend?.reclaim("critical");
@@ -54992,6 +55244,7 @@ async function startLocalServer(opts) {
54992
55244
  exit: (ptyId, exitCode, signal) => send2("term", { type: "term:exit", ptyId, exitCode, signal })
54993
55245
  };
54994
55246
  const heldSessions = /* @__PURE__ */ new Set();
55247
+ let focusedTab;
54995
55248
  let voiceControl = null;
54996
55249
  const unregisterSink = registerAgentSink((msg, origin2) => {
54997
55250
  const isCron = origin2 === "cron" && (MIRRORED_AGENT_EVENTS.has(msg.type) || msg.type === "session:created");
@@ -55023,13 +55276,16 @@ async function startLocalServer(opts) {
55023
55276
  unregisterSink();
55024
55277
  voiceControl?.stop("disconnected");
55025
55278
  voiceControl = null;
55026
- unwatchSessionClient(loopbackOrigin);
55027
55279
  clearBrowserOrigin(loopbackOrigin);
55028
- if (heldSessions.size) {
55280
+ if (heldSessions.size || focusedTab) {
55029
55281
  const b = peekSharedBackend();
55030
55282
  for (const sid of heldSessions)
55031
55283
  b?.setSessionKeepAlive?.(sid, false, loopbackOrigin);
55032
55284
  heldSessions.clear();
55285
+ if (focusedTab) {
55286
+ b?.setSessionKeepAlive?.(focusedTab, false, "tab");
55287
+ focusedTab = void 0;
55288
+ }
55033
55289
  }
55034
55290
  releaseClientPtys(termClientId);
55035
55291
  });
@@ -55062,8 +55318,11 @@ async function startLocalServer(opts) {
55062
55318
  ...msg.permissionMode ? { permissionMode: msg.permissionMode } : {},
55063
55319
  ...msg.effort ? { effort: msg.effort } : {},
55064
55320
  ...msg.model ? { model: msg.model } : {},
55065
- ...msg.jsonSchema ? { jsonSchema: msg.jsonSchema } : {}
55321
+ ...msg.jsonSchema ? { jsonSchema: msg.jsonSchema } : {},
55066
55322
  // M7 structured output
55323
+ ...msg.appendSystemPrompt ? { appendSystemPrompt: msg.appendSystemPrompt } : {},
55324
+ // mission hidden launch
55325
+ ...msg.missionSkillId ? { missionSkillId: msg.missionSkillId } : {}
55067
55326
  })).catch(() => {
55068
55327
  });
55069
55328
  break;
@@ -55088,18 +55347,6 @@ async function startLocalServer(opts) {
55088
55347
  send2("agent", { type: "session:summary", clientRequestId: msg.clientRequestId, ...res });
55089
55348
  break;
55090
55349
  }
55091
- case "session:watch":
55092
- void watchSession(msg.sessionId, loopbackOrigin, msg.projectDir, {
55093
- getMessages: (sid) => b.getMessages(sid),
55094
- isSessionBusy: (sid) => b.isSessionBusy?.(sid) ?? false,
55095
- resolveFile: (sid, dir) => b.resolveSessionFile?.(sid, dir) ?? Promise.resolve(null),
55096
- getSessionArtifacts: (sid) => b.getSessionArtifacts?.(sid) ?? Promise.resolve([]),
55097
- emit: (_key, m) => send2("agent", m)
55098
- });
55099
- break;
55100
- case "session:unwatch":
55101
- unwatchSession(msg.sessionId, loopbackOrigin);
55102
- break;
55103
55350
  case "session:fork": {
55104
55351
  const newId = await b.forkSession?.(msg.sessionId, { projectDir: msg.projectDir, upToMessageId: msg.upToMessageId, title: msg.title }).catch(() => null);
55105
55352
  if (newId)
@@ -55146,6 +55393,9 @@ async function startLocalServer(opts) {
55146
55393
  case "chat:elicitation":
55147
55394
  b.respondToElicitation?.(msg.requestId, { action: msg.action, content: msg.content }, loopbackOrigin);
55148
55395
  break;
55396
+ case "chat:ui_response":
55397
+ b.respondToUiRequest?.(msg.requestId, msg.response, loopbackOrigin);
55398
+ break;
55149
55399
  case "agent:mcp_reconnect":
55150
55400
  void b.reconnectMcpServer?.(msg.sessionId, msg.name);
55151
55401
  break;
@@ -55162,6 +55412,28 @@ async function startLocalServer(opts) {
55162
55412
  heldSessions.delete(msg.sessionId);
55163
55413
  b.setSessionKeepAlive?.(msg.sessionId, msg.keep, loopbackOrigin);
55164
55414
  break;
55415
+ case "session:focus": {
55416
+ const warmParams = mergeDaemonAgentSettings({
55417
+ message: "",
55418
+ url: msg.url ?? "",
55419
+ sessionId: msg.sessionId,
55420
+ projectDir: msg.projectDir ?? opts.hostRoot,
55421
+ sources: msg.sources,
55422
+ agents: msg.agents,
55423
+ browserId: msg.browserId ?? loopbackOrigin,
55424
+ origin: loopbackOrigin,
55425
+ ...msg.permissionMode ? { permissionMode: msg.permissionMode } : {},
55426
+ ...msg.effort ? { effort: msg.effort } : {},
55427
+ ...msg.model ? { model: msg.model } : {}
55428
+ });
55429
+ if (msg.focused === false) {
55430
+ void b.warmSession?.(warmParams);
55431
+ } else {
55432
+ focusedTab = msg.sessionId;
55433
+ void b.setActiveSession?.(msg.sessionId, warmParams);
55434
+ }
55435
+ break;
55436
+ }
55165
55437
  case "config:set":
55166
55438
  b.setConfig(msg.projectDir);
55167
55439
  refreshDaemonScheduling(b);
@@ -55560,7 +55832,7 @@ import { platform as platform2 } from "node:os";
55560
55832
 
55561
55833
  // ../../packages/host-core/dist/service/launchd.js
55562
55834
  import { writeFileSync as writeFileSync14, existsSync as existsSync17, mkdirSync as mkdirSync14, unlinkSync as unlinkSync3 } from "node:fs";
55563
- import { dirname as dirname15, join as join33 } from "node:path";
55835
+ import { dirname as dirname14, join as join33 } from "node:path";
55564
55836
  import { homedir as homedir14, userInfo } from "node:os";
55565
55837
 
55566
55838
  // ../../packages/host-core/dist/service/exec.js
@@ -55627,8 +55899,8 @@ ${env}
55627
55899
  }
55628
55900
  return {
55629
55901
  async install(spec) {
55630
- mkdirSync14(dirname15(plistPath()), { recursive: true });
55631
- mkdirSync14(dirname15(spec.logPath), { recursive: true });
55902
+ mkdirSync14(dirname14(plistPath()), { recursive: true });
55903
+ mkdirSync14(dirname14(spec.logPath), { recursive: true });
55632
55904
  writeFileSync14(plistPath(), buildPlist(spec));
55633
55905
  await run("launchctl", ["bootout", serviceTarget()]).catch(() => {
55634
55906
  });
@@ -55680,7 +55952,7 @@ ${env}
55680
55952
 
55681
55953
  // ../../packages/host-core/dist/service/systemd.js
55682
55954
  import { writeFileSync as writeFileSync15, existsSync as existsSync18, mkdirSync as mkdirSync15, unlinkSync as unlinkSync4 } from "node:fs";
55683
- import { dirname as dirname16, join as join34 } from "node:path";
55955
+ import { dirname as dirname15, join as join34 } from "node:path";
55684
55956
  import { homedir as homedir15, userInfo as userInfo2 } from "node:os";
55685
55957
  function shq(s) {
55686
55958
  return `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
@@ -55714,8 +55986,8 @@ WantedBy=default.target
55714
55986
  }
55715
55987
  return {
55716
55988
  async install(spec) {
55717
- mkdirSync15(dirname16(unitPath()), { recursive: true });
55718
- mkdirSync15(dirname16(spec.logPath), { recursive: true });
55989
+ mkdirSync15(dirname15(unitPath()), { recursive: true });
55990
+ mkdirSync15(dirname15(spec.logPath), { recursive: true });
55719
55991
  writeFileSync15(unitPath(), buildUnit(spec));
55720
55992
  await run("loginctl", ["enable-linger", userInfo2().username]).catch(() => {
55721
55993
  });
@@ -55760,7 +56032,7 @@ WantedBy=default.target
55760
56032
 
55761
56033
  // ../../packages/host-core/dist/service/windows.js
55762
56034
  import { writeFileSync as writeFileSync16, existsSync as existsSync19, mkdirSync as mkdirSync16, unlinkSync as unlinkSync5 } from "node:fs";
55763
- import { dirname as dirname17, join as join35 } from "node:path";
56035
+ import { dirname as dirname16, join as join35 } from "node:path";
55764
56036
  function makeWindowsTask(instanceId) {
55765
56037
  const task = `Fineguide\\TesseractHost${instanceId ? `-${instanceId}` : ""}`;
55766
56038
  const wrapperPath = () => join35(daemonDir(), `run-host${instanceId ? `-${instanceId}` : ""}.cmd`);
@@ -55778,8 +56050,8 @@ start "" /b ${cmd} >> "${oneLine2(spec.logPath).replace(/"/g, "")}" 2>&1\r
55778
56050
  }
55779
56051
  return {
55780
56052
  async install(spec) {
55781
- mkdirSync16(dirname17(spec.logPath), { recursive: true });
55782
- mkdirSync16(dirname17(wrapperPath()), { recursive: true });
56053
+ mkdirSync16(dirname16(spec.logPath), { recursive: true });
56054
+ mkdirSync16(dirname16(wrapperPath()), { recursive: true });
55783
56055
  writeFileSync16(wrapperPath(), buildWrapper(spec));
55784
56056
  await run("schtasks", ["/Create", "/TN", task, "/SC", "ONLOGON", "/RL", "LIMITED", "/F", "/TR", `"${wrapperPath()}"`]);
55785
56057
  await run("schtasks", ["/Run", "/TN", task]).catch(() => {
@@ -56272,7 +56544,7 @@ async function logs(instanceId, follow) {
56272
56544
  // src/cli.ts
56273
56545
  function pkgVersion() {
56274
56546
  try {
56275
- const p = join38(dirname18(fileURLToPath2(import.meta.url)), "..", "package.json");
56547
+ const p = join38(dirname17(fileURLToPath2(import.meta.url)), "..", "package.json");
56276
56548
  return JSON.parse(readFileSync15(p, "utf8")).version ?? "0.0.0";
56277
56549
  } catch {
56278
56550
  return "0.0.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tesseract-agent",
3
- "version": "0.4.19",
3
+ "version": "0.4.21",
4
4
  "license": "UNLICENSED",
5
5
  "publishConfig": {
6
6
  "access": "public",