neko-harness 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/neko.js CHANGED
@@ -79,7 +79,7 @@ function updateAction(input) {
79
79
  return "install";
80
80
  return "none";
81
81
  }
82
- var RUNNING_VERSION = "0.3.2", APPLICATION_PROTOCOL = 4, ApplicationProtocolError;
82
+ var RUNNING_VERSION = "0.3.3", APPLICATION_PROTOCOL = 4, ApplicationProtocolError;
83
83
  var init_application_version = __esm(() => {
84
84
  ApplicationProtocolError = class ApplicationProtocolError extends Error {
85
85
  constructor() {
@@ -713,6 +713,125 @@ var init_images = __esm(() => {
713
713
  };
714
714
  });
715
715
 
716
+ // packages/core/src/jobs.ts
717
+ import { defineDoc } from "@earendil-works/pi-durable";
718
+ function tailOf(text, maxBytes, maxLines) {
719
+ let bytes = encoder.encode(text);
720
+ if (bytes.length > maxBytes) {
721
+ let start = bytes.length - maxBytes;
722
+ while (start < bytes.length && (bytes[start] & 192) === 128)
723
+ start++;
724
+ bytes = bytes.subarray(start);
725
+ text = decoder.decode(bytes);
726
+ }
727
+ let index = text.length;
728
+ let seen = 0;
729
+ if (text.endsWith(`
730
+ `))
731
+ index--;
732
+ while (index > 0) {
733
+ const found = text.lastIndexOf(`
734
+ `, index - 1);
735
+ if (found < 0)
736
+ break;
737
+ if (++seen >= maxLines)
738
+ return text.slice(found + 1);
739
+ index = found;
740
+ }
741
+ return text;
742
+ }
743
+
744
+ class OutputBuffer {
745
+ #chunks = [];
746
+ #bytes = 0;
747
+ #total = 0;
748
+ #maxBytes;
749
+ #maxLines;
750
+ constructor(maxBytes = JOB_LIMITS.tailBytes, maxLines = JOB_LIMITS.tailLines) {
751
+ this.#maxBytes = maxBytes;
752
+ this.#maxLines = maxLines;
753
+ }
754
+ static fromTail(tail, totalBytes) {
755
+ const buffer = new OutputBuffer;
756
+ buffer.#chunks = [tail];
757
+ buffer.#bytes = byteLength(tail);
758
+ buffer.#total = totalBytes;
759
+ return buffer;
760
+ }
761
+ get totalBytes() {
762
+ return this.#total;
763
+ }
764
+ append(text) {
765
+ if (!text)
766
+ return;
767
+ const size = byteLength(text);
768
+ this.#total += size;
769
+ this.#bytes += size;
770
+ this.#chunks.push(text);
771
+ if (this.#bytes > this.#maxBytes * 2)
772
+ this.#trim();
773
+ }
774
+ #trim() {
775
+ const text = tailOf(this.#chunks.join(""), this.#maxBytes, this.#maxLines);
776
+ this.#chunks = [text];
777
+ this.#bytes = byteLength(text);
778
+ return text;
779
+ }
780
+ tail() {
781
+ return this.#trim();
782
+ }
783
+ read(since, maxBytes = JOB_LIMITS.readBytes) {
784
+ const tail = this.#trim();
785
+ const start = this.#total - this.#bytes;
786
+ const all = encoder.encode(tail);
787
+ let begin = since === undefined ? start : Math.min(Math.max(since, start), this.#total);
788
+ let bytes = all.subarray(begin - start);
789
+ let cut = Math.max(0, bytes.length - maxBytes);
790
+ while (cut < bytes.length && (bytes[cut] & 192) === 128)
791
+ cut++;
792
+ begin += cut;
793
+ bytes = bytes.subarray(cut);
794
+ const dropped = Math.max(0, begin - (since ?? 0));
795
+ return { text: decoder.decode(bytes), offset: this.#total, truncated: dropped > 0, dropped };
796
+ }
797
+ }
798
+ var JOB_LIMITS, JobsDoc, summarizeJobRecord = (job, outputBytes = job.totalBytes ?? 0) => {
799
+ const {
800
+ tail: _tail,
801
+ totalBytes: _total,
802
+ runnerId: _runner,
803
+ reporterId: _reporter,
804
+ stopOrigin: _origin,
805
+ ...rest
806
+ } = job;
807
+ return { ...rest, outputBytes };
808
+ }, isTerminalJob = (status) => status !== "running", encoder, decoder, byteLength = (text) => encoder.encode(text).length, reportTail = (text) => tailOf(text, JOB_LIMITS.reportBytes, JOB_LIMITS.reportLines);
809
+ var init_jobs = __esm(() => {
810
+ JOB_LIMITS = {
811
+ maxRunning: 4,
812
+ keepTerminal: 20,
813
+ startWindowMs: 2000,
814
+ tailBytes: 64 * 1024,
815
+ tailLines: 1000,
816
+ reportBytes: 4 * 1024,
817
+ reportLines: 40,
818
+ readBytes: 64 * 1024,
819
+ titleChars: 200,
820
+ commandBytes: 32 * 1024,
821
+ maxTimeoutSeconds: 7 * 24 * 3600
822
+ };
823
+ JobsDoc = defineDoc({
824
+ kind: "app.neko.jobs",
825
+ version: 1,
826
+ scope: "conversation",
827
+ history: "latest",
828
+ fork: "initial",
829
+ initial: () => ({ next: 1, items: [] })
830
+ });
831
+ encoder = new TextEncoder;
832
+ decoder = new TextDecoder;
833
+ });
834
+
716
835
  // packages/core/src/image-prompt.ts
717
836
  function addDefaultImagePrompt(content) {
718
837
  if (typeof content === "string" || !content.some((block) => block.type === "image") || content.some((block) => block.type === "text" && block.text.trim() !== ""))
@@ -975,6 +1094,17 @@ function projectChatView(view, modelLabel, options = {}) {
975
1094
  const items = [];
976
1095
  for (const entry of view.entries) {
977
1096
  const message = entry.model?.[0];
1097
+ const jobReport = entry.kind === NekoInput.kind ? entry.data?.jobReport : undefined;
1098
+ if (message?.role === "user" && jobReport) {
1099
+ items.push({
1100
+ type: "system",
1101
+ key: `e${entry.id}`,
1102
+ id: entry.id,
1103
+ text: userText(message.content),
1104
+ jobId: jobReport.jobId
1105
+ });
1106
+ continue;
1107
+ }
978
1108
  if (message?.role === "user") {
979
1109
  const defaultImagePrompt = entry.kind === NekoInput.kind ? savedDefaultImagePrompt(message.content, entry.data?.defaultImagePrompt) : undefined;
980
1110
  items.push({
@@ -1428,139 +1558,6 @@ var init_image_metadata = __esm(() => {
1428
1558
  });
1429
1559
  });
1430
1560
 
1431
- // packages/core/src/prompt/diagnostics.ts
1432
- var ignorePromptDiagnostic = () => {}, warnPromptDiagnostic = ({ path, message }) => {
1433
- console.warn(`[neko-prompt] ${JSON.stringify(path)}: ${message}`);
1434
- }, isMissing = (error) => typeof error === "object" && error !== null && ("code" in error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
1435
-
1436
- // packages/core/src/prompt/skills.ts
1437
- import { readdir, readFile as readFile4 } from "fs/promises";
1438
- import { basename as basename2, dirname as dirname5, join as join5 } from "path";
1439
- var MAX_NAME_LENGTH = 64, MAX_DESCRIPTION_LENGTH = 1024, parseSkillFile = (content, filePath, report = ignorePromptDiagnostic) => {
1440
- const skip = (message) => {
1441
- report({ path: filePath, message });
1442
- return null;
1443
- };
1444
- try {
1445
- const { frontmatter } = parseFrontmatter(content);
1446
- const description = frontmatter.description;
1447
- if (typeof description !== "string" || description.trim() === "")
1448
- return skip("Skill description is missing or empty; skipped.");
1449
- if (description.length > MAX_DESCRIPTION_LENGTH)
1450
- return skip("Skill description is too long; skipped.");
1451
- const name = typeof frontmatter.name === "string" && frontmatter.name.trim() !== "" ? frontmatter.name : basename2(dirname5(filePath));
1452
- if (name.length > MAX_NAME_LENGTH || /[\s\p{Cc}]/u.test(name))
1453
- return skip("Skill name is too long or contains whitespace/control characters; skipped.");
1454
- return {
1455
- name,
1456
- description,
1457
- filePath,
1458
- disableModelInvocation: frontmatter["disable-model-invocation"] === true
1459
- };
1460
- } catch {
1461
- return skip("Invalid skill frontmatter; skipped.");
1462
- }
1463
- }, scanSkillsDir = async (dir, report = ignorePromptDiagnostic, capture) => {
1464
- let entries;
1465
- try {
1466
- entries = await readdir(dir, { withFileTypes: true });
1467
- } catch (error) {
1468
- if (!isMissing(error))
1469
- report({ path: dir, message: "Cannot scan skills directory; skipped." });
1470
- return [];
1471
- }
1472
- const skills = [];
1473
- for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
1474
- if (!entry.isDirectory())
1475
- continue;
1476
- const filePath = join5(dir, entry.name, "SKILL.md");
1477
- let content;
1478
- try {
1479
- content = await readFile4(filePath, "utf8");
1480
- } catch (error) {
1481
- if (!isMissing(error))
1482
- report({ path: filePath, message: "Cannot read skill; skipped." });
1483
- continue;
1484
- }
1485
- const skill = parseSkillFile(content, filePath, report);
1486
- if (skill) {
1487
- skills.push(skill);
1488
- capture?.(skill, content);
1489
- }
1490
- }
1491
- return skills;
1492
- }, mergeSkills = (groups) => {
1493
- const byName = new Map;
1494
- for (const group of groups)
1495
- for (const skill of group)
1496
- byName.set(skill.name, skill);
1497
- return [...byName.values()];
1498
- }, escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;"), formatSkillsForPrompt = (skills) => {
1499
- const visible = skills.filter((skill) => !skill.disableModelInvocation);
1500
- if (visible.length === 0)
1501
- return "";
1502
- const lines = [
1503
- "The following skills provide specialized instructions for specific tasks.",
1504
- "Use the read tool to load a skill's file when the task matches its description.",
1505
- "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md) and use that absolute path in tool commands.",
1506
- "",
1507
- "<available_skills>"
1508
- ];
1509
- for (const skill of visible) {
1510
- lines.push(" <skill>");
1511
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
1512
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
1513
- lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
1514
- lines.push(" </skill>");
1515
- }
1516
- lines.push("</available_skills>");
1517
- return lines.join(`
1518
- `);
1519
- };
1520
- var init_skills = __esm(() => {
1521
- init_frontmatter();
1522
- });
1523
-
1524
- // packages/core/src/prompt/invoke.ts
1525
- import { dirname as dirname6 } from "path";
1526
- var expandSkillCommand = async (text, skills, readFile, report = warnPromptDiagnostic, metadata) => {
1527
- if (!text.startsWith("/skill:"))
1528
- return text;
1529
- const spaceIndex = text.indexOf(" ");
1530
- const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
1531
- const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
1532
- const skill = skills.find((s) => s.name === skillName);
1533
- if (!skill)
1534
- return text;
1535
- let body;
1536
- try {
1537
- body = stripFrontmatter(await readFile(skill.filePath)).trim();
1538
- } catch {
1539
- report({ path: skill.filePath, message: "Cannot expand known skill; original input preserved." });
1540
- return text;
1541
- }
1542
- metadata?.({
1543
- name: skill.name,
1544
- location: skill.filePath,
1545
- body: `References are relative to ${dirname6(skill.filePath)}.
1546
-
1547
- ${body}`,
1548
- args
1549
- });
1550
- const block = `<skill name="${escapeXml(skill.name)}" location="${escapeXml(skill.filePath)}">
1551
- References are relative to ${dirname6(skill.filePath)}.
1552
-
1553
- ${body}
1554
- </skill>`;
1555
- return args ? `${block}
1556
-
1557
- ${args}` : block;
1558
- };
1559
- var init_invoke = __esm(() => {
1560
- init_frontmatter();
1561
- init_skills();
1562
- });
1563
-
1564
1561
  // packages/core/src/subagent-usage.ts
1565
1562
  function subtractSubagentUsage(end, start) {
1566
1563
  const result = emptySubagentUsage();
@@ -1610,7 +1607,7 @@ var init_subagent_usage = __esm(() => {
1610
1607
  import {
1611
1608
  AssistantEntry,
1612
1609
  configure,
1613
- defineDoc,
1610
+ defineDoc as defineDoc2,
1614
1611
  defineExtension as defineExtension3,
1615
1612
  defineTask,
1616
1613
  defineTool as defineTool2,
@@ -1883,7 +1880,7 @@ function createSubagentExtension(prepare, discard) {
1883
1880
  thinkingLevel: agent.thinkingLevel,
1884
1881
  cwd: agent.cwd,
1885
1882
  extensions: agent.extensions.filter((e) => e.name !== extension.name),
1886
- tools: agent.tools.filter((t) => t.name !== "subagent" && (role === "coder" || READ_TOOLS.has(t.name) || role === "explore" && t.name === "bash"))
1883
+ tools: agent.tools.filter((t) => t.name !== "subagent" && t.name !== "job" && (role === "coder" || READ_TOOLS.has(t.name) || role === "explore" && t.name === "bash"))
1887
1884
  });
1888
1885
  Object.assign(await tx.doc(SubagentDoc, created.id), { role, parentId: api.conversationId });
1889
1886
  identity = {
@@ -2027,7 +2024,7 @@ var agentIdFor = (childId) => `agent-${childId}`, DelegationsDoc, SubagentsDoc,
2027
2024
  var init_subagents = __esm(() => {
2028
2025
  init_input();
2029
2026
  init_subagent_usage();
2030
- DelegationsDoc = defineDoc({
2027
+ DelegationsDoc = defineDoc2({
2031
2028
  kind: "app.neko.delegations",
2032
2029
  version: 1,
2033
2030
  scope: "conversation",
@@ -2035,7 +2032,7 @@ var init_subagents = __esm(() => {
2035
2032
  fork: "initial",
2036
2033
  initial: () => ({ items: [] })
2037
2034
  });
2038
- SubagentsDoc = defineDoc({
2035
+ SubagentsDoc = defineDoc2({
2039
2036
  kind: "app.neko.subagents",
2040
2037
  version: 1,
2041
2038
  scope: "conversation",
@@ -2043,7 +2040,7 @@ var init_subagents = __esm(() => {
2043
2040
  fork: "initial",
2044
2041
  initial: () => ({ items: [] })
2045
2042
  });
2046
- SubagentDoc = defineDoc({
2043
+ SubagentDoc = defineDoc2({
2047
2044
  kind: "app.neko.subagent",
2048
2045
  version: 1,
2049
2046
  scope: "conversation",
@@ -2051,7 +2048,7 @@ var init_subagents = __esm(() => {
2051
2048
  fork: "initial",
2052
2049
  initial: () => ({ role: null, parentId: null })
2053
2050
  });
2054
- SlotsDoc = defineDoc({
2051
+ SlotsDoc = defineDoc2({
2055
2052
  kind: "app.neko.subagent-slots",
2056
2053
  version: 1,
2057
2054
  scope: "session",
@@ -2119,8 +2116,573 @@ var init_subagents = __esm(() => {
2119
2116
  ]);
2120
2117
  });
2121
2118
 
2119
+ // packages/core/src/job-engine.ts
2120
+ import { withAbortSignal as withAbortSignal2 } from "@earendil-works/chord/context";
2121
+ import {
2122
+ defineExtension as defineExtension4,
2123
+ defineTask as defineTask2,
2124
+ defineTool as defineTool3
2125
+ } from "@earendil-works/pi-durable";
2126
+ import { Type as Type3 } from "typebox";
2127
+
2128
+ class JobEntry {
2129
+ controller = new AbortController;
2130
+ buffer = new OutputBuffer;
2131
+ windowOpen = false;
2132
+ suppressReport = false;
2133
+ exited;
2134
+ #resolve;
2135
+ constructor() {
2136
+ this.exited = new Promise((resolve) => {
2137
+ this.#resolve = resolve;
2138
+ });
2139
+ }
2140
+ finish() {
2141
+ this.#resolve();
2142
+ }
2143
+ }
2144
+
2145
+ class JobRegistry {
2146
+ #entries = new Map;
2147
+ static key(conversationId, jobId) {
2148
+ return `${conversationId}:${jobId}`;
2149
+ }
2150
+ ensure(conversationId, jobId) {
2151
+ const key = JobRegistry.key(conversationId, jobId);
2152
+ let entry = this.#entries.get(key);
2153
+ if (!entry) {
2154
+ entry = new JobEntry;
2155
+ this.#entries.set(key, entry);
2156
+ }
2157
+ return entry;
2158
+ }
2159
+ get(conversationId, jobId) {
2160
+ return this.#entries.get(JobRegistry.key(conversationId, jobId));
2161
+ }
2162
+ delete(conversationId, jobId) {
2163
+ this.#entries.delete(JobRegistry.key(conversationId, jobId));
2164
+ }
2165
+ abortAll() {
2166
+ for (const entry of this.#entries.values())
2167
+ entry.controller.abort();
2168
+ }
2169
+ abortConversation(conversationId) {
2170
+ for (const [key, entry] of this.#entries)
2171
+ if (key.startsWith(`${conversationId}:`))
2172
+ entry.controller.abort();
2173
+ }
2174
+ }
2175
+ function reportText(job, tail) {
2176
+ const body = reportTail(tail);
2177
+ return [
2178
+ `[Background job ${job.jobId} "${job.title}" ${describe(job)}]`,
2179
+ `Command: ${job.command}`,
2180
+ body ? `Last output (use the job tool's output action with jobId "${job.jobId}" for more):
2181
+ ${body}` : "It produced no output."
2182
+ ].join(`
2183
+ `);
2184
+ }
2185
+ async function settleJob(tx, conversationId, jobId, outcome, tail, quiet, reporter) {
2186
+ const state = await tx.doc(JobsDoc, conversationId);
2187
+ const job = state.items.find((item) => item.jobId === jobId);
2188
+ if (!job || isTerminalJob(job.status))
2189
+ return;
2190
+ job.status = outcome.status;
2191
+ job.exitCode = outcome.exitCode;
2192
+ if (outcome.reason)
2193
+ job.reason = outcome.reason;
2194
+ job.endedAt = Date.now();
2195
+ job.tail = tail.text;
2196
+ job.totalBytes = tail.totalBytes;
2197
+ if (!quiet && (job.status !== "stopped" || job.stopOrigin === "user"))
2198
+ job.reporterId = await tx.createTask(reporter, { jobId, text: reportText(job, tail.text), status: job.status, exitCode: job.exitCode }, { ownership: { kind: "conversation" }, conversationId, background: true });
2199
+ const terminal = state.items.filter((item) => isTerminalJob(item.status));
2200
+ if (terminal.length > JOB_LIMITS.keepTerminal) {
2201
+ const drop = new Set(terminal.sort((a, b) => (a.endedAt ?? 0) - (b.endedAt ?? 0)).slice(0, terminal.length - JOB_LIMITS.keepTerminal).map((item) => item.jobId));
2202
+ state.items = state.items.filter((item) => !drop.has(item.jobId));
2203
+ }
2204
+ return job;
2205
+ }
2206
+ function createReporter() {
2207
+ return defineTask2({
2208
+ name: "app.neko.job-reporter",
2209
+ version: 1,
2210
+ initial: () => ({ phase: "report" }),
2211
+ phases: {
2212
+ report: async (task, runtime, ctx) => {
2213
+ const target = await runtime.conversation(runtime.conversationId, ctx);
2214
+ await target.submit({
2215
+ type: "input",
2216
+ content: task.input.text,
2217
+ whenBusy: "followUp",
2218
+ requestId: `job-report:${task.input.jobId}`,
2219
+ entry: {
2220
+ kind: NekoInput.kind,
2221
+ data: {
2222
+ at: Date.now(),
2223
+ prompt: task.input.text,
2224
+ images: [],
2225
+ jobReport: { jobId: task.input.jobId, status: task.input.status, exitCode: task.input.exitCode }
2226
+ }
2227
+ }
2228
+ }, ctx);
2229
+ await runtime.commit(() => ({ status: "terminal", outcome: { status: "completed", result: null } }), ctx);
2230
+ }
2231
+ },
2232
+ abort: (_task, runtime, ctx) => runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), ctx)
2233
+ });
2234
+ }
2235
+
2236
+ class JobManager {
2237
+ registry = new JobRegistry;
2238
+ #reporter = createReporter();
2239
+ #runner = this.#createRunner();
2240
+ extension;
2241
+ constructor() {
2242
+ this.extension = this.#createExtension();
2243
+ }
2244
+ #closing = false;
2245
+ #createRunner() {
2246
+ const registry = this.registry;
2247
+ const reporter = this.#reporter;
2248
+ return defineTask2({
2249
+ name: "app.neko.job-runner",
2250
+ version: 1,
2251
+ initial: () => ({ phase: "start" }),
2252
+ phases: {
2253
+ start: (_task, runtime, ctx) => runtime.commit(() => ({ status: "running", checkpoint: { phase: "exec" } }), ctx),
2254
+ exec: async (task, runtime, ctx) => {
2255
+ const { jobId, command, cwd, timeoutSeconds } = task.input;
2256
+ const entry = registry.ensure(runtime.conversationId, jobId);
2257
+ let outcome;
2258
+ try {
2259
+ const env = await runtime.env(ctx);
2260
+ if (!env) {
2261
+ outcome = { status: "failed", exitCode: null, reason: "spawn-error" };
2262
+ } else {
2263
+ const result = await env.exec(command, {
2264
+ ...cwd ? { cwd } : {},
2265
+ ...timeoutSeconds === undefined ? {} : { timeout: timeoutSeconds },
2266
+ onOutput: (text) => entry.buffer.append(text)
2267
+ }, withAbortSignal2(entry.controller.signal, ctx));
2268
+ if (result.ok)
2269
+ outcome = {
2270
+ status: result.value.exitCode === 0 ? "exited" : "failed",
2271
+ exitCode: result.value.exitCode
2272
+ };
2273
+ else if (result.error.code === "timeout")
2274
+ outcome = { status: "failed", exitCode: null, reason: "timeout" };
2275
+ else if (result.error.code === "aborted")
2276
+ outcome = { status: "stopped", exitCode: null, reason: "aborted" };
2277
+ else
2278
+ outcome = { status: "failed", exitCode: null, reason: "spawn-error" };
2279
+ }
2280
+ if (this.#closing) {
2281
+ await new Promise((resolve) => {
2282
+ if (!ctx.abortSignal || ctx.abortSignal.aborted)
2283
+ resolve();
2284
+ else
2285
+ ctx.abortSignal.addEventListener("abort", () => resolve(), { once: true });
2286
+ });
2287
+ throw ctx.abortSignal?.reason ?? new Error("Job runner interrupted by shutdown");
2288
+ }
2289
+ const quiet = entry.windowOpen;
2290
+ if (quiet)
2291
+ entry.suppressReport = true;
2292
+ const tail = { text: entry.buffer.tail(), totalBytes: entry.buffer.totalBytes };
2293
+ await runtime.commit(async (tx) => {
2294
+ const job = await settleJob(tx, runtime.conversationId, jobId, outcome, tail, quiet, reporter);
2295
+ return {
2296
+ status: "terminal",
2297
+ outcome: { status: "completed", result: { status: job?.status ?? outcome.status } }
2298
+ };
2299
+ }, ctx);
2300
+ } finally {
2301
+ registry.delete(runtime.conversationId, jobId);
2302
+ entry.finish();
2303
+ }
2304
+ }
2305
+ },
2306
+ abort: async (task, runtime, ctx) => {
2307
+ const { jobId } = task.input;
2308
+ const entry = registry.get(runtime.conversationId, jobId);
2309
+ entry?.controller.abort();
2310
+ const restart = task.abortReason === "restart";
2311
+ const outcome = restart ? { status: "lost", exitCode: null, reason: "restart" } : { status: "stopped", exitCode: null, reason: "aborted" };
2312
+ try {
2313
+ await runtime.commit(async (tx) => {
2314
+ const known = (await tx.doc(JobsDoc, runtime.conversationId)).items.find((item) => item.jobId === jobId);
2315
+ const tail = entry ? { text: entry.buffer.tail(), totalBytes: entry.buffer.totalBytes } : { text: known?.tail ?? "", totalBytes: known?.totalBytes ?? 0 };
2316
+ await settleJob(tx, runtime.conversationId, jobId, outcome, tail, false, reporter);
2317
+ return { status: "terminal", outcome: { status: "aborted" } };
2318
+ }, ctx);
2319
+ } finally {
2320
+ registry.delete(runtime.conversationId, jobId);
2321
+ entry?.finish();
2322
+ }
2323
+ }
2324
+ });
2325
+ }
2326
+ #createExtension() {
2327
+ return defineExtension4({
2328
+ name: "neko-jobs",
2329
+ tasks: [this.#runner, this.#reporter],
2330
+ tools: [
2331
+ defineTool3({
2332
+ name: "job",
2333
+ description: "Run long-lived commands (dev servers, watchers, long builds) in the background. start: command (optional title, timeout seconds) returns a jobId immediately; the command has no stdin. output: jobId (optional since offset) reads the output tail or what is new. stop: jobId terminates the whole process tree. list: show this session's jobs. When a job ends you receive a report; do not poll. At most 4 jobs run at once.",
2334
+ parameters: parameters2,
2335
+ replay: "unsafe",
2336
+ execute: async (input, api, context) => {
2337
+ const args = input;
2338
+ if ((await api.snapshot(SubagentDoc, api.conversationId, context))?.role)
2339
+ throw new Error("Subagents cannot use jobs.");
2340
+ const io = {
2341
+ commit: (change) => api.commit(change, context),
2342
+ state: async () => api.snapshot(JobsDoc, api.conversationId, context)
2343
+ };
2344
+ const kind = args.action;
2345
+ if (kind === "list")
2346
+ return this.#text(this.#listText((await io.state())?.items ?? []));
2347
+ if (kind === "output") {
2348
+ const jobId = stringField(args.jobId, "jobId");
2349
+ const since = args.since;
2350
+ if (since !== undefined && (!Number.isInteger(since) || since < 0))
2351
+ throw new Error("Invalid job arguments: since must be a non-negative integer.");
2352
+ return this.#text(await this.output(io, api.conversationId, jobId, since));
2353
+ }
2354
+ if (kind === "stop") {
2355
+ const jobId = stringField(args.jobId, "jobId");
2356
+ const job = await this.stop(io, api.conversationId, jobId, "model", context);
2357
+ return this.#text(`Job ${job.jobId} ${describe(job)}.`);
2358
+ }
2359
+ if (kind !== "start")
2360
+ throw new Error("Invalid job arguments: action must be start, output, stop or list.");
2361
+ const command = stringField(args.command, "command");
2362
+ if (Buffer.byteLength(command) > JOB_LIMITS.commandBytes)
2363
+ throw new Error("Invalid job arguments: command is too long.");
2364
+ const timeout = args.timeout;
2365
+ if (timeout !== undefined && (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0 || timeout > JOB_LIMITS.maxTimeoutSeconds))
2366
+ throw new Error("Invalid job arguments: timeout must be a positive number of seconds.");
2367
+ const title = typeof args.title === "string" && args.title.trim() ? args.title.trim() : command;
2368
+ const agent = await api.agent(context);
2369
+ return this.#text(await this.start(io, api.conversationId, agent.cwd ?? "", {
2370
+ command,
2371
+ title: title.slice(0, JOB_LIMITS.titleChars),
2372
+ ...timeout === undefined ? {} : { timeoutSeconds: timeout }
2373
+ }, context));
2374
+ }
2375
+ })
2376
+ ]
2377
+ });
2378
+ }
2379
+ #text(text) {
2380
+ return { output: [{ type: "text", text }] };
2381
+ }
2382
+ #listText(items) {
2383
+ if (!items.length)
2384
+ return "No jobs.";
2385
+ return items.map((job) => `${job.jobId} [${describe(job)}] "${job.title}" \u2014 ${job.command}`).join(`
2386
+ `);
2387
+ }
2388
+ async start(io, conversationId, cwd, args, context) {
2389
+ let allocated;
2390
+ let entry;
2391
+ let created;
2392
+ try {
2393
+ created = await io.commit(async (tx) => {
2394
+ const state = await tx.doc(JobsDoc, conversationId);
2395
+ if (state.items.filter((item) => item.status === "running").length >= JOB_LIMITS.maxRunning)
2396
+ throw new Error(`Too many running jobs (limit ${JOB_LIMITS.maxRunning}). Stop one with the job tool's stop action first.`);
2397
+ const jobId = `job-${state.next++}`;
2398
+ const record = {
2399
+ jobId,
2400
+ title: args.title,
2401
+ command: args.command,
2402
+ cwd,
2403
+ status: "running",
2404
+ startedAt: Date.now(),
2405
+ endedAt: null,
2406
+ exitCode: null,
2407
+ ...args.timeoutSeconds === undefined ? {} : { timeoutSeconds: args.timeoutSeconds },
2408
+ runnerId: 0
2409
+ };
2410
+ allocated = jobId;
2411
+ entry = this.registry.ensure(conversationId, jobId);
2412
+ entry.windowOpen = true;
2413
+ record.runnerId = await tx.createTask(this.#runner, {
2414
+ jobId,
2415
+ command: args.command,
2416
+ cwd,
2417
+ ...args.timeoutSeconds === undefined ? {} : { timeoutSeconds: args.timeoutSeconds }
2418
+ }, { ownership: { kind: "conversation" }, conversationId, background: true, abandonOnRestart: true });
2419
+ state.items.push(record);
2420
+ return plain(record);
2421
+ });
2422
+ } catch (error) {
2423
+ if (allocated)
2424
+ this.registry.delete(conversationId, allocated);
2425
+ throw error;
2426
+ }
2427
+ const live = entry;
2428
+ await Promise.race([live.exited, delay(JOB_LIMITS.startWindowMs, context.abortSignal)]);
2429
+ live.windowOpen = false;
2430
+ if (live.suppressReport)
2431
+ await live.exited;
2432
+ const job = (await io.state())?.items.find((item) => item.jobId === created.jobId) ?? created;
2433
+ const read = live.buffer.read();
2434
+ return this.#render(job, read.text, read.offset, read.dropped);
2435
+ }
2436
+ #render(job, text, offset, dropped) {
2437
+ const lines = [`Job ${job.jobId} ${describe(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
2438
+ if (dropped > 0)
2439
+ lines.push(`(${dropped} bytes of earlier output were dropped)`);
2440
+ lines.push(text ? `Output (next offset ${offset}):
2441
+ ${text}` : `No new output (next offset ${offset}).`);
2442
+ return lines.join(`
2443
+ `);
2444
+ }
2445
+ async output(io, conversationId, jobId, since) {
2446
+ const job = await this.#find(io, jobId);
2447
+ const output = this.read(conversationId, job, since);
2448
+ return this.#render(job, output.text, output.offset, output.dropped);
2449
+ }
2450
+ async readOutput(io, conversationId, jobId, since) {
2451
+ const job = await this.#find(io, jobId);
2452
+ const output = this.read(conversationId, job, since);
2453
+ return { job: summarizeJobRecord(job, output.offset), output };
2454
+ }
2455
+ async#find(io, jobId) {
2456
+ const job = (await io.state())?.items.find((item) => item.jobId === jobId);
2457
+ if (!job)
2458
+ throw new Error(`Unknown job ${jobId}.`);
2459
+ return job;
2460
+ }
2461
+ async list(io, conversationId) {
2462
+ return ((await io.state())?.items ?? []).map((job) => summarizeJobRecord(job, this.registry.get(conversationId, job.jobId)?.buffer.totalBytes ?? job.totalBytes ?? 0));
2463
+ }
2464
+ read(conversationId, job, since) {
2465
+ const entry = isTerminalJob(job.status) ? undefined : this.registry.get(conversationId, job.jobId);
2466
+ return (entry?.buffer ?? OutputBuffer.fromTail(job.tail ?? "", job.totalBytes ?? 0)).read(since);
2467
+ }
2468
+ async stop(io, conversationId, jobId, origin, context) {
2469
+ let live;
2470
+ const found = await io.commit(async (tx) => {
2471
+ live = undefined;
2472
+ const job = (await tx.doc(JobsDoc, conversationId)).items.find((item) => item.jobId === jobId);
2473
+ if (!job)
2474
+ throw new Error(`Unknown job ${jobId}.`);
2475
+ if (isTerminalJob(job.status))
2476
+ return { job: plain(job) };
2477
+ job.stopOrigin ??= origin;
2478
+ live = this.registry.get(conversationId, jobId);
2479
+ if (live)
2480
+ return { job: plain(job) };
2481
+ const settled = await settleJob(tx, conversationId, jobId, { status: "stopped", exitCode: null }, { text: job.tail ?? "", totalBytes: job.totalBytes ?? 0 }, false, this.#reporter);
2482
+ return { job: plain(settled ?? job) };
2483
+ });
2484
+ const entry = live;
2485
+ if (entry) {
2486
+ entry.controller.abort();
2487
+ await Promise.race([entry.exited, delay(1e4, context?.abortSignal)]);
2488
+ return (await io.state())?.items.find((item) => item.jobId === jobId) ?? found.job;
2489
+ }
2490
+ return found.job;
2491
+ }
2492
+ async stopAll(io, conversationId, origin = "archive") {
2493
+ const running = ((await io.state())?.items ?? []).filter((item) => item.status === "running");
2494
+ for (const job of running)
2495
+ await this.stop(io, conversationId, job.jobId, origin);
2496
+ }
2497
+ shutdown() {
2498
+ this.#closing = true;
2499
+ this.registry.abortAll();
2500
+ }
2501
+ }
2502
+ var plain = (value) => JSON.parse(JSON.stringify(value)), statusWord, describe = (job) => {
2503
+ const parts = [statusWord[job.status]];
2504
+ if (job.exitCode !== null)
2505
+ parts.push(`exit code ${job.exitCode}`);
2506
+ if (job.reason === "timeout")
2507
+ parts.push(`timed out after ${job.timeoutSeconds} seconds`);
2508
+ if (job.reason === "spawn-error")
2509
+ parts.push("could not start");
2510
+ if (job.reason === "restart")
2511
+ parts.push("the process was lost when the gateway restarted and was not re-run");
2512
+ return parts.join(", ");
2513
+ }, delay = (ms, signal) => new Promise((resolve) => {
2514
+ const timer = setTimeout(done, ms);
2515
+ function done() {
2516
+ clearTimeout(timer);
2517
+ signal?.removeEventListener("abort", done);
2518
+ resolve();
2519
+ }
2520
+ if (signal?.aborted)
2521
+ done();
2522
+ else
2523
+ signal?.addEventListener("abort", done, { once: true });
2524
+ }), action, parameters2, stringField = (value, name) => {
2525
+ if (typeof value !== "string" || !value.trim())
2526
+ throw new Error(`Invalid job arguments: ${name} must be a nonempty string.`);
2527
+ return value;
2528
+ };
2529
+ var init_job_engine = __esm(() => {
2530
+ init_input();
2531
+ init_jobs();
2532
+ init_subagents();
2533
+ statusWord = {
2534
+ running: "running",
2535
+ exited: "exited",
2536
+ failed: "failed",
2537
+ stopped: "stopped",
2538
+ lost: "lost"
2539
+ };
2540
+ action = Type3.Union([Type3.Literal("start"), Type3.Literal("output"), Type3.Literal("stop"), Type3.Literal("list")]);
2541
+ parameters2 = Type3.Object({
2542
+ action,
2543
+ command: Type3.Optional(Type3.String({ description: "start: shell command to run in the session's working directory." })),
2544
+ title: Type3.Optional(Type3.String({ description: "start: short label shown in the job list." })),
2545
+ timeout: Type3.Optional(Type3.Number({ description: "start: optional timeout in seconds; no default timeout." })),
2546
+ jobId: Type3.Optional(Type3.String({ description: "output/stop: the job id returned by start or list." })),
2547
+ since: Type3.Optional(Type3.Integer({ description: "output: resume after this offset, as returned by the previous output call." }))
2548
+ });
2549
+ });
2550
+
2551
+ // packages/core/src/prompt/diagnostics.ts
2552
+ var ignorePromptDiagnostic = () => {}, warnPromptDiagnostic = ({ path, message }) => {
2553
+ console.warn(`[neko-prompt] ${JSON.stringify(path)}: ${message}`);
2554
+ }, isMissing = (error) => typeof error === "object" && error !== null && ("code" in error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
2555
+
2556
+ // packages/core/src/prompt/skills.ts
2557
+ import { readdir, readFile as readFile4 } from "fs/promises";
2558
+ import { basename as basename2, dirname as dirname5, join as join5 } from "path";
2559
+ var MAX_NAME_LENGTH = 64, MAX_DESCRIPTION_LENGTH = 1024, parseSkillFile = (content, filePath, report = ignorePromptDiagnostic) => {
2560
+ const skip = (message) => {
2561
+ report({ path: filePath, message });
2562
+ return null;
2563
+ };
2564
+ try {
2565
+ const { frontmatter } = parseFrontmatter(content);
2566
+ const description = frontmatter.description;
2567
+ if (typeof description !== "string" || description.trim() === "")
2568
+ return skip("Skill description is missing or empty; skipped.");
2569
+ if (description.length > MAX_DESCRIPTION_LENGTH)
2570
+ return skip("Skill description is too long; skipped.");
2571
+ const name = typeof frontmatter.name === "string" && frontmatter.name.trim() !== "" ? frontmatter.name : basename2(dirname5(filePath));
2572
+ if (name.length > MAX_NAME_LENGTH || /[\s\p{Cc}]/u.test(name))
2573
+ return skip("Skill name is too long or contains whitespace/control characters; skipped.");
2574
+ return {
2575
+ name,
2576
+ description,
2577
+ filePath,
2578
+ disableModelInvocation: frontmatter["disable-model-invocation"] === true
2579
+ };
2580
+ } catch {
2581
+ return skip("Invalid skill frontmatter; skipped.");
2582
+ }
2583
+ }, scanSkillsDir = async (dir, report = ignorePromptDiagnostic, capture) => {
2584
+ let entries;
2585
+ try {
2586
+ entries = await readdir(dir, { withFileTypes: true });
2587
+ } catch (error) {
2588
+ if (!isMissing(error))
2589
+ report({ path: dir, message: "Cannot scan skills directory; skipped." });
2590
+ return [];
2591
+ }
2592
+ const skills = [];
2593
+ for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
2594
+ if (!entry.isDirectory())
2595
+ continue;
2596
+ const filePath = join5(dir, entry.name, "SKILL.md");
2597
+ let content;
2598
+ try {
2599
+ content = await readFile4(filePath, "utf8");
2600
+ } catch (error) {
2601
+ if (!isMissing(error))
2602
+ report({ path: filePath, message: "Cannot read skill; skipped." });
2603
+ continue;
2604
+ }
2605
+ const skill = parseSkillFile(content, filePath, report);
2606
+ if (skill) {
2607
+ skills.push(skill);
2608
+ capture?.(skill, content);
2609
+ }
2610
+ }
2611
+ return skills;
2612
+ }, mergeSkills = (groups) => {
2613
+ const byName = new Map;
2614
+ for (const group of groups)
2615
+ for (const skill of group)
2616
+ byName.set(skill.name, skill);
2617
+ return [...byName.values()];
2618
+ }, escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;"), formatSkillsForPrompt = (skills) => {
2619
+ const visible = skills.filter((skill) => !skill.disableModelInvocation);
2620
+ if (visible.length === 0)
2621
+ return "";
2622
+ const lines = [
2623
+ "The following skills provide specialized instructions for specific tasks.",
2624
+ "Use the read tool to load a skill's file when the task matches its description.",
2625
+ "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md) and use that absolute path in tool commands.",
2626
+ "",
2627
+ "<available_skills>"
2628
+ ];
2629
+ for (const skill of visible) {
2630
+ lines.push(" <skill>");
2631
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
2632
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
2633
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
2634
+ lines.push(" </skill>");
2635
+ }
2636
+ lines.push("</available_skills>");
2637
+ return lines.join(`
2638
+ `);
2639
+ };
2640
+ var init_skills = __esm(() => {
2641
+ init_frontmatter();
2642
+ });
2643
+
2644
+ // packages/core/src/prompt/invoke.ts
2645
+ import { dirname as dirname6 } from "path";
2646
+ var expandSkillCommand = async (text, skills, readFile, report = warnPromptDiagnostic, metadata) => {
2647
+ if (!text.startsWith("/skill:"))
2648
+ return text;
2649
+ const spaceIndex = text.indexOf(" ");
2650
+ const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
2651
+ const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
2652
+ const skill = skills.find((s) => s.name === skillName);
2653
+ if (!skill)
2654
+ return text;
2655
+ let body;
2656
+ try {
2657
+ body = stripFrontmatter(await readFile(skill.filePath)).trim();
2658
+ } catch {
2659
+ report({ path: skill.filePath, message: "Cannot expand known skill; original input preserved." });
2660
+ return text;
2661
+ }
2662
+ metadata?.({
2663
+ name: skill.name,
2664
+ location: skill.filePath,
2665
+ body: `References are relative to ${dirname6(skill.filePath)}.
2666
+
2667
+ ${body}`,
2668
+ args
2669
+ });
2670
+ const block = `<skill name="${escapeXml(skill.name)}" location="${escapeXml(skill.filePath)}">
2671
+ References are relative to ${dirname6(skill.filePath)}.
2672
+
2673
+ ${body}
2674
+ </skill>`;
2675
+ return args ? `${block}
2676
+
2677
+ ${args}` : block;
2678
+ };
2679
+ var init_invoke = __esm(() => {
2680
+ init_frontmatter();
2681
+ init_skills();
2682
+ });
2683
+
2122
2684
  // packages/core/src/prompt/prompt.ts
2123
- import { defineExtension as defineExtension4, section } from "@earendil-works/pi-durable";
2685
+ import { defineExtension as defineExtension5, section } from "@earendil-works/pi-durable";
2124
2686
  var KEYS, PREAMBLE = "You are an expert coding assistant operating inside neko, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.", TOOL_GUIDELINES, GENERIC_RULES, buildRules = (toolNames) => {
2125
2687
  const rules = [];
2126
2688
  const seen = new Set;
@@ -2154,7 +2716,7 @@ ${content}
2154
2716
  };
2155
2717
  return sections;
2156
2718
  };
2157
- return defineExtension4({
2719
+ return defineExtension5({
2158
2720
  name: "neko-prompt",
2159
2721
  sections: KEYS.map((key) => section(key, async (input, context) => {
2160
2722
  let pending = built.get(input);
@@ -2194,6 +2756,9 @@ var init_prompt = __esm(() => {
2194
2756
  "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions."
2195
2757
  ],
2196
2758
  bash: ["Use bash for file operations like ls, rg, find"],
2759
+ job: [
2760
+ 'Use job {action:"start", command, title?, timeout?} for long-running commands (dev servers, watchers, long builds or tests) instead of bash, which would block. Jobs have no stdin. start returns within 2 seconds, with the early result if the command already ended; a job that ends later reports back to you automatically, so do not poll. Read new output with {action:"output", jobId, since?} using the offset from the previous call, list jobs with {action:"list"}, end one with {action:"stop", jobId}. Run short commands with bash.'
2761
+ ],
2197
2762
  web_search: ["Use web_search for current information from the internet instead of guessing."],
2198
2763
  web_extract: ["Use web_extract to read the full content of a web page when you have its URL."]
2199
2764
  };
@@ -2330,9 +2895,9 @@ var createResourceSnapshots = (load, submission) => {
2330
2895
  import { stripVTControlCharacters } from "util";
2331
2896
  import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
2332
2897
  import {
2333
- defineDoc as defineDoc2,
2334
- defineExtension as defineExtension5,
2335
- defineTask as defineTask2,
2898
+ defineDoc as defineDoc3,
2899
+ defineExtension as defineExtension6,
2900
+ defineTask as defineTask3,
2336
2901
  InboxDoc,
2337
2902
  LiveDoc as LiveDoc2
2338
2903
  } from "@earendil-works/pi-durable";
@@ -2376,7 +2941,7 @@ function mergeUsage(base, extra) {
2376
2941
  function namingTask() {
2377
2942
  let active = 0;
2378
2943
  const waits = new Set;
2379
- return defineTask2({
2944
+ return defineTask3({
2380
2945
  name: "app.neko.name",
2381
2946
  version: 1,
2382
2947
  initial: () => ({ phase: "wait", until: Date.now() + 100 }),
@@ -2500,7 +3065,7 @@ class SessionMetadataManager {
2500
3065
  this.storage = storage;
2501
3066
  this.context = context;
2502
3067
  this.report = report;
2503
- registry.install(defineExtension5({ name: "neko-naming", tasks: [this.task] }));
3068
+ registry.install(defineExtension6({ name: "neko-naming", tasks: [this.task] }));
2504
3069
  this.#unsubscribe = harness.subscribeCommits(({ changes }) => {
2505
3070
  const ids = new Set;
2506
3071
  const submissions = new Map;
@@ -2641,7 +3206,7 @@ class SessionMetadataManager {
2641
3206
  var SessionMetadataDoc;
2642
3207
  var init_session_metadata = __esm(() => {
2643
3208
  init_input();
2644
- SessionMetadataDoc = defineDoc2({
3209
+ SessionMetadataDoc = defineDoc3({
2645
3210
  kind: "app.neko.session-metadata",
2646
3211
  version: 1,
2647
3212
  scope: "conversation",
@@ -2671,8 +3236,8 @@ import { randomUUID } from "crypto";
2671
3236
  import { realpath as realpath2 } from "fs/promises";
2672
3237
  import {
2673
3238
  configure as configure2,
2674
- defineDoc as defineDoc3,
2675
- defineExtension as defineExtension6,
3239
+ defineDoc as defineDoc4,
3240
+ defineExtension as defineExtension7,
2676
3241
  hook as hook2,
2677
3242
  ToolTask as ToolTask2,
2678
3243
  wrapTool
@@ -2778,7 +3343,7 @@ class ToolApprovals {
2778
3343
  }
2779
3344
  }
2780
3345
  function createApprovalExtension(service) {
2781
- return defineExtension6({
3346
+ return defineExtension7({
2782
3347
  name: "neko-tool-approvals",
2783
3348
  hooks: [
2784
3349
  hook2(ToolTask2, {
@@ -2843,7 +3408,7 @@ async function prepareApprovalConversations(harness, ids, extension, context) {
2843
3408
  var ApprovalsDoc;
2844
3409
  var init_tool_approvals = __esm(() => {
2845
3410
  init_subagents();
2846
- ApprovalsDoc = defineDoc3({
3411
+ ApprovalsDoc = defineDoc4({
2847
3412
  kind: "app.neko.approvals",
2848
3413
  version: 1,
2849
3414
  scope: "conversation",
@@ -2854,8 +3419,8 @@ var init_tool_approvals = __esm(() => {
2854
3419
  });
2855
3420
 
2856
3421
  // packages/core/src/web-search.ts
2857
- import { Type as Type3 } from "@earendil-works/pi-ai";
2858
- import { defineExtension as defineExtension7, defineTool as defineTool3 } from "@earendil-works/pi-durable";
3422
+ import { Type as Type4 } from "@earendil-works/pi-ai";
3423
+ import { defineExtension as defineExtension8, defineTool as defineTool4 } from "@earendil-works/pi-durable";
2859
3424
  function redact(text, key) {
2860
3425
  for (const secret of key ? [key, encodeURIComponent(key)] : [])
2861
3426
  text = text.replaceAll(secret, "[redacted]");
@@ -2961,15 +3526,15 @@ function urlKey(value) {
2961
3526
  }
2962
3527
  }
2963
3528
  function createWebSearchTools(resolveKey, fetchImpl = fetch) {
2964
- return defineExtension7({
3529
+ return defineExtension8({
2965
3530
  name: "neko-web-search",
2966
3531
  tools: [
2967
- defineTool3({
3532
+ defineTool4({
2968
3533
  name: "web_search",
2969
3534
  description: "Search the live web for current or unknown information. Returns ranked sources (title, URL, bounded excerpts). Use only when up-to-date or external information is needed; use web_extract to read a known URL.",
2970
- parameters: Type3.Object({
2971
- query: Type3.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
2972
- maxResults: Type3.Optional(Type3.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
3535
+ parameters: Type4.Object({
3536
+ query: Type4.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
3537
+ maxResults: Type4.Optional(Type4.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
2973
3538
  }),
2974
3539
  async execute({ query, maxResults }, _api, context) {
2975
3540
  try {
@@ -2994,11 +3559,11 @@ ${truncate(String(item.url ?? ""), 300)}
2994
3559
  }
2995
3560
  }
2996
3561
  }),
2997
- defineTool3({
3562
+ defineTool4({
2998
3563
  name: "web_extract",
2999
3564
  description: "Extract bounded markdown excerpts from web pages by URL. Every requested URL has a result or failure entry; long content is truncated.",
3000
- parameters: Type3.Object({
3001
- urls: Type3.Array(Type3.String({ maxLength: 2048, description: "http(s) URL" }), {
3565
+ parameters: Type4.Object({
3566
+ urls: Type4.Array(Type4.String({ maxLength: 2048, description: "http(s) URL" }), {
3002
3567
  minItems: 1,
3003
3568
  maxItems: MAX_URLS,
3004
3569
  description: `URLs to extract (1-${MAX_URLS})`
@@ -3051,7 +3616,7 @@ var init_web_search = __esm(() => {
3051
3616
  // packages/core/src/workspace-files.ts
3052
3617
  import { realpath as realpath3, stat as stat2 } from "fs/promises";
3053
3618
  import { basename as basename3, isAbsolute as isAbsolute2, relative, resolve as resolve3, sep } from "path";
3054
- import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal2 } from "@earendil-works/chord/context";
3619
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
3055
3620
  import { getOrThrow as getOrThrow2 } from "@earendil-works/pi-durable/env";
3056
3621
  import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
3057
3622
  function relativeQuery(query) {
@@ -3205,7 +3770,7 @@ class WorkspaceFiles {
3205
3770
  for (const user of [...current.users])
3206
3771
  this.#closeWatcher(user);
3207
3772
  }
3208
- }, withAbortSignal2(current.controller.signal, BACKGROUND_CONTEXT2));
3773
+ }, withAbortSignal3(current.controller.signal, BACKGROUND_CONTEXT2));
3209
3774
  if (result.ok) {
3210
3775
  current.watcher = result.value;
3211
3776
  if (current.controller.signal.aborted)
@@ -3233,7 +3798,7 @@ class WorkspaceFiles {
3233
3798
  async#scan(item) {
3234
3799
  const controller = new AbortController;
3235
3800
  item.controller = controller;
3236
- const context = withAbortSignal2(controller.signal, BACKGROUND_CONTEXT2);
3801
+ const context = withAbortSignal3(controller.signal, BACKGROUND_CONTEXT2);
3237
3802
  while (this.#active >= this.limits.concurrent) {
3238
3803
  await new Promise((resolve, reject) => {
3239
3804
  const wake = () => {
@@ -3420,7 +3985,7 @@ import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
3420
3985
  import {
3421
3986
  AssistantEntry as AssistantEntry2,
3422
3987
  createRegistry,
3423
- defineDoc as defineDoc4,
3988
+ defineDoc as defineDoc5,
3424
3989
  Harness,
3425
3990
  LiveDoc as importLiveDoc,
3426
3991
  MemoryStorage,
@@ -3480,6 +4045,8 @@ async function createNekoRuntime(options = {}) {
3480
4045
  registry.install(createSubagentExtension(async (id, requestId, cwd) => {
3481
4046
  resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
3482
4047
  }, resourceSnapshots.discard));
4048
+ const jobs = new JobManager;
4049
+ registry.install(jobs.extension);
3483
4050
  for (const extension of options.extensions ?? [])
3484
4051
  registry.install(extension);
3485
4052
  let approvals;
@@ -3543,6 +4110,10 @@ async function createNekoRuntime(options = {}) {
3543
4110
  if (target)
3544
4111
  await registerWorkspace(savedWorkspace((await target.agent(context)).cwd));
3545
4112
  }
4113
+ const jobIo = (id) => ({
4114
+ commit: (change) => harness.commit(change, context),
4115
+ state: () => harness.snapshot(JobsDoc, id, context)
4116
+ });
3546
4117
  let lastActivity = 0;
3547
4118
  for (const record of existing)
3548
4119
  lastActivity = Math.max(lastActivity, (await harness.snapshot(ActivityDoc, record.id, context))?.lastUsedAt ?? 0);
@@ -3565,6 +4136,7 @@ async function createNekoRuntime(options = {}) {
3565
4136
  const cleanup = () => {
3566
4137
  if (!closing)
3567
4138
  closing = (async () => {
4139
+ jobs.shutdown();
3568
4140
  await workspaceFiles.close();
3569
4141
  await metadata.close();
3570
4142
  })();
@@ -3697,6 +4269,16 @@ async function createNekoRuntime(options = {}) {
3697
4269
  compactionRecord() {
3698
4270
  return readCompaction(harness, storage, target.id, context);
3699
4271
  },
4272
+ jobs() {
4273
+ return jobs.list(jobIo(target.id), target.id);
4274
+ },
4275
+ jobOutput(jobId, since) {
4276
+ return jobs.readOutput(jobIo(target.id), target.id, jobId, since);
4277
+ },
4278
+ async stopJob(jobId) {
4279
+ const job = await jobs.stop(jobIo(target.id), target.id, jobId, "user", context);
4280
+ return summarizeJobRecord(job);
4281
+ },
3700
4282
  async compact(instructions) {
3701
4283
  const agent = await target.agent(context);
3702
4284
  if (agent.model?.provider === "deepseek" && !await models.checkAuth("deepseek"))
@@ -3888,6 +4470,7 @@ async function createNekoRuntime(options = {}) {
3888
4470
  getConversation,
3889
4471
  async archiveConversation(id) {
3890
4472
  await (await sessionFor(id)).abort();
4473
+ await jobs.stopAll(jobIo(id), id);
3891
4474
  await metadata.archive(id);
3892
4475
  await harness.commit(async (tx) => {
3893
4476
  (await tx.doc(ArchivedDoc, id)).archived = true;
@@ -4001,6 +4584,8 @@ var init_runtime = __esm(() => {
4001
4584
  init_image_prompt();
4002
4585
  init_images();
4003
4586
  init_input();
4587
+ init_job_engine();
4588
+ init_jobs();
4004
4589
  init_projection();
4005
4590
  init_invoke();
4006
4591
  init_prompt();
@@ -4014,7 +4599,7 @@ var init_runtime = __esm(() => {
4014
4599
  init_workspace_files();
4015
4600
  DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
4016
4601
  DEFAULT_STORAGE_PATH = join7(homedir5(), ".neko", "neko.sqlite");
4017
- ArchivedDoc = defineDoc4({
4602
+ ArchivedDoc = defineDoc5({
4018
4603
  kind: "app.neko.conversation",
4019
4604
  version: 1,
4020
4605
  scope: "conversation",
@@ -4022,7 +4607,7 @@ var init_runtime = __esm(() => {
4022
4607
  fork: "initial",
4023
4608
  initial: () => ({ archived: false })
4024
4609
  });
4025
- WorkspacesDoc = defineDoc4({
4610
+ WorkspacesDoc = defineDoc5({
4026
4611
  kind: "app.neko.workspaces",
4027
4612
  version: 1,
4028
4613
  scope: "conversation",
@@ -4030,7 +4615,7 @@ var init_runtime = __esm(() => {
4030
4615
  fork: "initial",
4031
4616
  initial: () => ({ items: [] })
4032
4617
  });
4033
- ActivityDoc = defineDoc4({
4618
+ ActivityDoc = defineDoc5({
4034
4619
  kind: "app.neko.activity",
4035
4620
  version: 1,
4036
4621
  scope: "conversation",
@@ -4057,6 +4642,7 @@ var init_src = __esm(() => {
4057
4642
  init_config();
4058
4643
  init_credentials();
4059
4644
  init_images();
4645
+ init_jobs();
4060
4646
  init_projection();
4061
4647
  init_frontmatter();
4062
4648
  init_runtime();
@@ -4177,7 +4763,7 @@ var sessionName = (session, empty = "New session") => session.title || session.f
4177
4763
 
4178
4764
  // packages/gateway/src/contract.ts
4179
4765
  import { method, remote } from "@earendil-works/chord";
4180
- import { Type as Type4 } from "typebox";
4766
+ import { Type as Type5 } from "typebox";
4181
4767
  function contract(name, def) {
4182
4768
  const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
4183
4769
  key,
@@ -4188,21 +4774,21 @@ function contract(name, def) {
4188
4774
  ]));
4189
4775
  return remote(name, { ...def, variants });
4190
4776
  }
4191
- var rpc = (args, result) => [args, result], str, num, bool, nil, integer, nullableString, phase, level, thinking, model, change, usage, image, skill, call, DelegationSchema, SubagentSummarySchema, ChatViewSchema, SessionSummarySchema, DirectoryStateSchema, WorkspaceSummarySchema, WorkspaceStateSchema, credentialStatus, CredentialStateSchema, AttachmentSchema, chatInput, NekoSessionDirectory, NekoSessionManagement, resize, inputLimits, modelInfo, NekoModels, imageChunk, NekoSubagent, NekoChat, browse, page, NekoWorkspaces, provider, NekoCredentials, FileCandidatesSchema, NekoWorkspaceFiles, marketSkill, preview, scope, installResult, NekoSkillMarket, ApplicationInfoSchema, NekoApplicationInfo;
4777
+ var rpc = (args, result) => [args, result], str, num, bool, nil, integer, nullableString, phase, level, thinking, model, change, usage, image, skill, call, DelegationSchema, SubagentSummarySchema, jobStatus, JobSummarySchema, JobOutputSchema, ChatViewSchema, SessionSummarySchema, DirectoryStateSchema, WorkspaceSummarySchema, WorkspaceStateSchema, credentialStatus, CredentialStateSchema, AttachmentSchema, chatInput, NekoSessionDirectory, NekoSessionManagement, resize, inputLimits, modelInfo, NekoModels, imageChunk, NekoSubagent, NekoChat, browse, page, NekoWorkspaces, provider, NekoCredentials, FileCandidatesSchema, NekoWorkspaceFiles, marketSkill, preview, scope, installResult, NekoSkillMarket, ApplicationInfoSchema, NekoApplicationInfo;
4192
4778
  var init_contract = __esm(() => {
4193
- str = Type4.String();
4194
- num = Type4.Number();
4195
- bool = Type4.Boolean();
4196
- nil = Type4.Null();
4197
- integer = Type4.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
4198
- nullableString = Type4.Union([str, nil]);
4199
- phase = Type4.Enum(["pending", "running", "done"]);
4200
- level = Type4.Enum(["project", "user", "builtin"]);
4201
- thinking = Type4.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
4202
- model = Type4.Object({ provider: str, modelId: str });
4203
- change = Type4.Object({ model: Type4.Optional(model), thinkingLevel: Type4.Optional(thinking) }, { additionalProperties: false });
4204
- usage = Type4.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
4205
- image = Type4.Object({
4779
+ str = Type5.String();
4780
+ num = Type5.Number();
4781
+ bool = Type5.Boolean();
4782
+ nil = Type5.Null();
4783
+ integer = Type5.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
4784
+ nullableString = Type5.Union([str, nil]);
4785
+ phase = Type5.Enum(["pending", "running", "done"]);
4786
+ level = Type5.Enum(["project", "user", "builtin"]);
4787
+ thinking = Type5.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
4788
+ model = Type5.Object({ provider: str, modelId: str });
4789
+ change = Type5.Object({ model: Type5.Optional(model), thinkingLevel: Type5.Optional(thinking) }, { additionalProperties: false });
4790
+ usage = Type5.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
4791
+ image = Type5.Object({
4206
4792
  name: str,
4207
4793
  mimeType: str,
4208
4794
  width: integer,
@@ -4211,8 +4797,8 @@ var init_contract = __esm(() => {
4211
4797
  entryId: integer,
4212
4798
  contentIndex: integer
4213
4799
  });
4214
- skill = Type4.Object({ name: str, location: str, body: str, args: str });
4215
- call = Type4.Object({
4800
+ skill = Type5.Object({ name: str, location: str, body: str, args: str });
4801
+ call = Type5.Object({
4216
4802
  callId: str,
4217
4803
  name: str,
4218
4804
  summary: str,
@@ -4220,94 +4806,116 @@ var init_contract = __esm(() => {
4220
4806
  phase,
4221
4807
  output: nullableString,
4222
4808
  diff: nullableString,
4223
- diffAdded: Type4.Union([num, nil]),
4224
- diffRemoved: Type4.Union([num, nil]),
4225
- diffTruncated: Type4.Optional(bool)
4809
+ diffAdded: Type5.Union([num, nil]),
4810
+ diffRemoved: Type5.Union([num, nil]),
4811
+ diffTruncated: Type5.Optional(bool)
4226
4812
  });
4227
- DelegationSchema = Type4.Object({
4813
+ DelegationSchema = Type5.Object({
4228
4814
  parentId: integer,
4229
- agentId: Type4.Optional(str),
4230
- round: Type4.Optional(integer),
4231
- runnerId: Type4.Optional(integer),
4232
- inputEntryId: Type4.Optional(integer),
4233
- answerEntryId: Type4.Optional(integer),
4234
- roundUsage: Type4.Optional(Type4.Union([usage, nil])),
4815
+ agentId: Type5.Optional(str),
4816
+ round: Type5.Optional(integer),
4817
+ runnerId: Type5.Optional(integer),
4818
+ inputEntryId: Type5.Optional(integer),
4819
+ answerEntryId: Type5.Optional(integer),
4820
+ roundUsage: Type5.Optional(Type5.Union([usage, nil])),
4235
4821
  taskId: integer,
4236
4822
  callId: str,
4237
4823
  childId: integer,
4238
- role: Type4.Enum(["explore", "plan", "coder"]),
4824
+ role: Type5.Enum(["explore", "plan", "coder"]),
4239
4825
  title: str,
4240
4826
  createdAt: num,
4241
- startedAt: Type4.Union([num, nil]),
4242
- endedAt: Type4.Union([num, nil]),
4243
- status: Type4.Enum(["queued", "running", "completed", "failed", "cancelled"]),
4827
+ startedAt: Type5.Union([num, nil]),
4828
+ endedAt: Type5.Union([num, nil]),
4829
+ status: Type5.Enum(["queued", "running", "completed", "failed", "cancelled"]),
4244
4830
  report: nullableString,
4245
- approvalCount: Type4.Optional(integer),
4246
- historyThrough: Type4.Optional(integer),
4247
- historyUsage: Type4.Optional(usage)
4831
+ approvalCount: Type5.Optional(integer),
4832
+ historyThrough: Type5.Optional(integer),
4833
+ historyUsage: Type5.Optional(usage)
4248
4834
  });
4249
- SubagentSummarySchema = Type4.Object({
4835
+ SubagentSummarySchema = Type5.Object({
4250
4836
  agentId: str,
4251
4837
  parentId: integer,
4252
4838
  childId: integer,
4253
- role: Type4.Enum(["explore", "plan", "coder"]),
4839
+ role: Type5.Enum(["explore", "plan", "coder"]),
4254
4840
  title: str,
4255
- status: Type4.Enum(["idle", "queued", "running"]),
4256
- activeRoundId: Type4.Union([integer, nil]),
4841
+ status: Type5.Enum(["idle", "queued", "running"]),
4842
+ activeRoundId: Type5.Union([integer, nil]),
4257
4843
  lastRoundId: integer,
4258
- lastReport: Type4.Optional(nullableString),
4844
+ lastReport: Type5.Optional(nullableString),
4259
4845
  roundCount: integer,
4260
4846
  taskTitle: str,
4261
4847
  approvalCount: integer,
4262
4848
  createdAt: num,
4263
- historyThrough: Type4.Optional(integer),
4264
- historyUsage: Type4.Optional(usage)
4849
+ historyThrough: Type5.Optional(integer),
4850
+ historyUsage: Type5.Optional(usage)
4265
4851
  });
4266
- ChatViewSchema = Type4.Object({
4267
- historyBefore: Type4.Optional(Type4.Union([integer, nil])),
4268
- agents: Type4.Optional(Type4.Array(SubagentSummarySchema)),
4269
- rounds: Type4.Optional(Type4.Array(DelegationSchema)),
4270
- subagents: Type4.Optional(Type4.Array(DelegationSchema)),
4271
- taskUsage: Type4.Optional(usage),
4852
+ jobStatus = Type5.Enum(["running", "exited", "failed", "stopped", "lost"]);
4853
+ JobSummarySchema = Type5.Object({
4854
+ jobId: str,
4855
+ title: str,
4856
+ command: str,
4857
+ cwd: str,
4858
+ status: jobStatus,
4859
+ startedAt: num,
4860
+ endedAt: Type5.Union([num, nil]),
4861
+ exitCode: Type5.Union([num, nil]),
4862
+ reason: Type5.Optional(Type5.Enum(["timeout", "spawn-error", "restart", "aborted"])),
4863
+ timeoutSeconds: Type5.Optional(num),
4864
+ outputBytes: integer
4865
+ });
4866
+ JobOutputSchema = Type5.Object({
4867
+ job: JobSummarySchema,
4868
+ text: str,
4869
+ offset: integer,
4870
+ truncated: bool,
4871
+ dropped: integer
4872
+ });
4873
+ ChatViewSchema = Type5.Object({
4874
+ historyBefore: Type5.Optional(Type5.Union([integer, nil])),
4875
+ agents: Type5.Optional(Type5.Array(SubagentSummarySchema)),
4876
+ jobs: Type5.Optional(Type5.Array(JobSummarySchema)),
4877
+ rounds: Type5.Optional(Type5.Array(DelegationSchema)),
4878
+ subagents: Type5.Optional(Type5.Array(DelegationSchema)),
4879
+ taskUsage: Type5.Optional(usage),
4272
4880
  model: str,
4273
- agent: Type4.Object({ model: Type4.Union([model, nil]), thinkingLevel: Type4.Union([thinking, nil]) }),
4881
+ agent: Type5.Object({ model: Type5.Union([model, nil]), thinkingLevel: Type5.Union([thinking, nil]) }),
4274
4882
  cwd: str,
4275
4883
  busy: bool,
4276
4884
  status: nullableString,
4277
4885
  usage,
4278
- contextUsage: Type4.Optional(Type4.Object({ tokens: Type4.Union([num, nil]), window: num, percent: Type4.Union([num, nil]) })),
4279
- compaction: Type4.Optional(Type4.Object({
4280
- failure: Type4.Optional(Type4.Object({ taskId: integer, reason: Type4.Enum(["manual", "threshold", "overflow"]), error: str })),
4281
- active: Type4.Array(Type4.Object({
4886
+ contextUsage: Type5.Optional(Type5.Object({ tokens: Type5.Union([num, nil]), window: num, percent: Type5.Union([num, nil]) })),
4887
+ compaction: Type5.Optional(Type5.Object({
4888
+ failure: Type5.Optional(Type5.Object({ taskId: integer, reason: Type5.Enum(["manual", "threshold", "overflow"]), error: str })),
4889
+ active: Type5.Array(Type5.Object({
4282
4890
  taskId: integer,
4283
- reason: Type4.Enum(["manual", "threshold", "overflow"]),
4891
+ reason: Type5.Enum(["manual", "threshold", "overflow"]),
4284
4892
  blocking: bool,
4285
4893
  attempt: integer,
4286
- retry: Type4.Optional(Type4.Object({ at: num, error: str }))
4894
+ retry: Type5.Optional(Type5.Object({ at: num, error: str }))
4287
4895
  })),
4288
- latest: Type4.Union([
4896
+ latest: Type5.Union([
4289
4897
  nil,
4290
- Type4.Object({
4898
+ Type5.Object({
4291
4899
  taskId: integer,
4292
- phase: Type4.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
4900
+ phase: Type5.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
4293
4901
  at: num
4294
4902
  })
4295
4903
  ])
4296
4904
  })),
4297
- items: Type4.Array(Type4.Union([
4298
- Type4.Object({
4299
- type: Type4.Literal("user"),
4300
- order: Type4.Optional(num),
4905
+ items: Type5.Array(Type5.Union([
4906
+ Type5.Object({
4907
+ type: Type5.Literal("user"),
4908
+ order: Type5.Optional(num),
4301
4909
  key: str,
4302
4910
  id: integer,
4303
4911
  text: str,
4304
- images: Type4.Optional(Type4.Array(image)),
4305
- skill: Type4.Optional(skill),
4306
- defaultImagePrompt: Type4.Optional(Type4.Object({ version: Type4.Integer({ minimum: 1 }), text: str }))
4912
+ images: Type5.Optional(Type5.Array(image)),
4913
+ skill: Type5.Optional(skill),
4914
+ defaultImagePrompt: Type5.Optional(Type5.Object({ version: Type5.Integer({ minimum: 1 }), text: str }))
4307
4915
  }),
4308
- Type4.Object({
4309
- type: Type4.Literal("assistant"),
4310
- order: Type4.Optional(num),
4916
+ Type5.Object({
4917
+ type: Type5.Literal("assistant"),
4918
+ order: Type5.Optional(num),
4311
4919
  key: str,
4312
4920
  id: integer,
4313
4921
  thinking: str,
@@ -4316,11 +4924,11 @@ var init_contract = __esm(() => {
4316
4924
  errorMessage: nullableString,
4317
4925
  note: nullableString
4318
4926
  }),
4319
- Type4.Object({
4320
- type: Type4.Literal("tool"),
4321
- order: Type4.Optional(num),
4927
+ Type5.Object({
4928
+ type: Type5.Literal("tool"),
4929
+ order: Type5.Optional(num),
4322
4930
  key: str,
4323
- images: Type4.Optional(Type4.Array(image)),
4931
+ images: Type5.Optional(Type5.Array(image)),
4324
4932
  callId: str,
4325
4933
  name: str,
4326
4934
  summary: str,
@@ -4329,54 +4937,61 @@ var init_contract = __esm(() => {
4329
4937
  isError: bool,
4330
4938
  output: nullableString,
4331
4939
  result: nullableString,
4332
- durationMs: Type4.Union([num, nil]),
4940
+ durationMs: Type5.Union([num, nil]),
4333
4941
  note: nullableString,
4334
4942
  diff: nullableString,
4335
- diffAdded: Type4.Union([num, nil]),
4336
- diffRemoved: Type4.Union([num, nil]),
4337
- diffTruncated: Type4.Optional(bool)
4943
+ diffAdded: Type5.Union([num, nil]),
4944
+ diffRemoved: Type5.Union([num, nil]),
4945
+ diffTruncated: Type5.Optional(bool)
4338
4946
  }),
4339
- Type4.Object({ type: Type4.Literal("system"), order: Type4.Optional(num), key: str, id: integer, text: str })
4947
+ Type5.Object({
4948
+ type: Type5.Literal("system"),
4949
+ order: Type5.Optional(num),
4950
+ key: str,
4951
+ id: integer,
4952
+ text: str,
4953
+ jobId: Type5.Optional(str)
4954
+ })
4340
4955
  ])),
4341
- streaming: Type4.Union([nil, Type4.Object({ thinking: str, text: str, toolCalls: Type4.Array(call) })])
4956
+ streaming: Type5.Union([nil, Type5.Object({ thinking: str, text: str, toolCalls: Type5.Array(call) })])
4342
4957
  });
4343
- SessionSummarySchema = Type4.Object({
4958
+ SessionSummarySchema = Type5.Object({
4344
4959
  workspaceRoot: str,
4345
4960
  lastUsedAt: num,
4346
4961
  title: str,
4347
4962
  firstPromptPreview: str,
4348
4963
  createdAt: num,
4349
- lastMessageAt: Type4.Union([num, nil]),
4350
- model: Type4.Union([model, nil]),
4351
- status: Type4.Enum(["idle", "running", "error"]),
4964
+ lastMessageAt: Type5.Union([num, nil]),
4965
+ model: Type5.Union([model, nil]),
4966
+ status: Type5.Enum(["idle", "running", "error"]),
4352
4967
  id: str
4353
4968
  });
4354
- DirectoryStateSchema = Type4.Object({ revision: integer, sessions: Type4.Array(SessionSummarySchema) });
4355
- WorkspaceSummarySchema = Type4.Object({ path: str, createdAt: num });
4356
- WorkspaceStateSchema = Type4.Object({ revision: integer, workspaces: Type4.Array(WorkspaceSummarySchema) });
4357
- credentialStatus = Type4.Object({
4969
+ DirectoryStateSchema = Type5.Object({ revision: integer, sessions: Type5.Array(SessionSummarySchema) });
4970
+ WorkspaceSummarySchema = Type5.Object({ path: str, createdAt: num });
4971
+ WorkspaceStateSchema = Type5.Object({ revision: integer, workspaces: Type5.Array(WorkspaceSummarySchema) });
4972
+ credentialStatus = Type5.Object({
4358
4973
  savedConfigured: bool,
4359
4974
  environmentConfigured: bool,
4360
- source: Type4.Enum(["saved", "environment", "runtime", "keyless", "none"])
4975
+ source: Type5.Enum(["saved", "environment", "runtime", "keyless", "none"])
4361
4976
  });
4362
- CredentialStateSchema = Type4.Object({
4977
+ CredentialStateSchema = Type5.Object({
4363
4978
  revision: integer,
4364
- statuses: Type4.Object({ deepseek: credentialStatus, tavily: credentialStatus })
4979
+ statuses: Type5.Object({ deepseek: credentialStatus, tavily: credentialStatus })
4365
4980
  });
4366
- AttachmentSchema = Type4.Object({ sessionId: str, attachmentId: str });
4367
- chatInput = Type4.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
4981
+ AttachmentSchema = Type5.Object({ sessionId: str, attachmentId: str });
4982
+ chatInput = Type5.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
4368
4983
  NekoSessionDirectory = contract("neko.session-directory", {
4369
4984
  version: 1,
4370
- input: Type4.Object({}, { additionalProperties: false }),
4985
+ input: Type5.Object({}, { additionalProperties: false }),
4371
4986
  variants: { ready: { state: { state: DirectoryStateSchema } } }
4372
4987
  });
4373
4988
  NekoSessionManagement = contract("neko.session-management", {
4374
4989
  version: 1,
4375
- input: Type4.Object({}, { additionalProperties: false }),
4990
+ input: Type5.Object({}, { additionalProperties: false }),
4376
4991
  variants: {
4377
4992
  ready: {
4378
4993
  methods: {
4379
- create: rpc(Type4.Object({ ...change.properties, cwd: Type4.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
4994
+ create: rpc(Type5.Object({ ...change.properties, cwd: Type5.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
4380
4995
  archive: rpc(str, nil),
4381
4996
  attach: rpc(str, AttachmentSchema),
4382
4997
  detach: rpc(nil, nil),
@@ -4385,30 +5000,30 @@ var init_contract = __esm(() => {
4385
5000
  }
4386
5001
  }
4387
5002
  });
4388
- resize = Type4.Object({
4389
- maxWidth: Type4.Optional(integer),
4390
- maxHeight: Type4.Optional(integer),
4391
- maxBytes: Type4.Optional(integer),
4392
- jpegQuality: Type4.Optional(integer)
5003
+ resize = Type5.Object({
5004
+ maxWidth: Type5.Optional(integer),
5005
+ maxHeight: Type5.Optional(integer),
5006
+ maxBytes: Type5.Optional(integer),
5007
+ jpegQuality: Type5.Optional(integer)
4393
5008
  });
4394
- inputLimits = Type4.Object({
4395
- maxRequestBytes: Type4.Optional(integer),
4396
- images: Type4.Optional(Type4.Object({
4397
- resize: Type4.Optional(resize),
4398
- maxPerMessage: Type4.Optional(integer),
4399
- maxPerRequest: Type4.Optional(integer)
5009
+ inputLimits = Type5.Object({
5010
+ maxRequestBytes: Type5.Optional(integer),
5011
+ images: Type5.Optional(Type5.Object({
5012
+ resize: Type5.Optional(resize),
5013
+ maxPerMessage: Type5.Optional(integer),
5014
+ maxPerRequest: Type5.Optional(integer)
4400
5015
  }))
4401
5016
  });
4402
- modelInfo = Type4.Object({
5017
+ modelInfo = Type5.Object({
4403
5018
  provider: str,
4404
5019
  modelId: str,
4405
5020
  name: str,
4406
5021
  contextWindow: num,
4407
- input: Type4.Array(str),
4408
- inputLimits: Type4.Optional(inputLimits),
5022
+ input: Type5.Array(str),
5023
+ inputLimits: Type5.Optional(inputLimits),
4409
5024
  reasoning: bool,
4410
- supportedThinkingLevels: Type4.Array(thinking),
4411
- thinkingLevelAdjustments: Type4.Object({
5025
+ supportedThinkingLevels: Type5.Array(thinking),
5026
+ thinkingLevelAdjustments: Type5.Object({
4412
5027
  off: thinking,
4413
5028
  minimal: thinking,
4414
5029
  low: thinking,
@@ -4420,31 +5035,31 @@ var init_contract = __esm(() => {
4420
5035
  });
4421
5036
  NekoModels = contract("neko.models", {
4422
5037
  version: 1,
4423
- input: Type4.Object({}, { additionalProperties: false }),
4424
- variants: { ready: { methods: { list: rpc(nil, Type4.Array(modelInfo)) } } }
5038
+ input: Type5.Object({}, { additionalProperties: false }),
5039
+ variants: { ready: { methods: { list: rpc(nil, Type5.Array(modelInfo)) } } }
4425
5040
  });
4426
- imageChunk = Type4.Object({ data: str, total: integer, mimeType: str });
5041
+ imageChunk = Type5.Object({ data: str, total: integer, mimeType: str });
4427
5042
  NekoSubagent = contract("neko.subagent", {
4428
5043
  version: 1,
4429
- input: Type4.Object({
5044
+ input: Type5.Object({
4430
5045
  sessionId: str,
4431
5046
  attachmentId: str,
4432
- taskId: Type4.Optional(integer),
4433
- agentId: Type4.Optional(str)
5047
+ taskId: Type5.Optional(integer),
5048
+ agentId: Type5.Optional(str)
4434
5049
  }),
4435
5050
  variants: {
4436
5051
  ready: {
4437
5052
  state: { revision: integer },
4438
5053
  methods: {
4439
- chunk: rpc(Type4.Object({
4440
- kind: Type4.Enum(["view", "history"]),
4441
- before: Type4.Optional(integer),
4442
- token: Type4.Optional(integer),
5054
+ chunk: rpc(Type5.Object({
5055
+ kind: Type5.Enum(["view", "history"]),
5056
+ before: Type5.Optional(integer),
5057
+ token: Type5.Optional(integer),
4443
5058
  offset: integer
4444
- }), Type4.Object({ token: integer, data: str, next: Type4.Union([integer, nil]) })),
4445
- getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
4446
- decideApproval: rpc(Type4.Object({ id: integer, version: str, decision: Type4.Enum(["allowed", "denied"]) }), Type4.Enum(["pending", "allowed", "denied", "cancelled"])),
4447
- stop: rpc(Type4.Union([nil, Type4.Object({ taskId: integer })]), nil)
5059
+ }), Type5.Object({ token: integer, data: str, next: Type5.Union([integer, nil]) })),
5060
+ getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
5061
+ decideApproval: rpc(Type5.Object({ id: integer, version: str, decision: Type5.Enum(["allowed", "denied"]) }), Type5.Enum(["pending", "allowed", "denied", "cancelled"])),
5062
+ stop: rpc(Type5.Union([nil, Type5.Object({ taskId: integer })]), nil)
4448
5063
  }
4449
5064
  }
4450
5065
  }
@@ -4456,79 +5071,81 @@ var init_contract = __esm(() => {
4456
5071
  ready: {
4457
5072
  state: { view: ChatViewSchema },
4458
5073
  methods: {
4459
- submit: rpc(Type4.Object({
5074
+ submit: rpc(Type5.Object({
4460
5075
  content: str,
4461
- images: Type4.Optional(Type4.Array(str, { maxItems: 4 })),
4462
- requestId: Type4.Optional(str),
4463
- whenBusy: Type4.Optional(Type4.Enum(["steer", "followUp"]))
5076
+ images: Type5.Optional(Type5.Array(str, { maxItems: 4 })),
5077
+ requestId: Type5.Optional(str),
5078
+ whenBusy: Type5.Optional(Type5.Enum(["steer", "followUp"]))
4464
5079
  }, { additionalProperties: false }), nil),
4465
5080
  abort: rpc(nil, nil),
4466
5081
  usage: rpc(nil, usage),
4467
- skills: rpc(nil, Type4.Array(Type4.Object({ name: str, description: str, location: str, level }))),
5082
+ skills: rpc(nil, Type5.Array(Type5.Object({ name: str, description: str, location: str, level }))),
5083
+ jobOutput: rpc(Type5.Object({ jobId: str, since: Type5.Optional(integer) }, { additionalProperties: false }), JobOutputSchema),
5084
+ jobStop: rpc(Type5.Object({ jobId: str }, { additionalProperties: false }), JobSummarySchema),
4468
5085
  configure: rpc(change, nil),
4469
- compact: rpc(Type4.Object({ instructions: Type4.Optional(str) }, { additionalProperties: false }), nil),
4470
- importImage: rpc(str, Type4.Object({ id: str, name: str })),
4471
- draftImage: rpc(Type4.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
4472
- beginImage: rpc(Type4.Object({ name: str, mimeType: str, size: Type4.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
4473
- appendImage: rpc(Type4.Object({ id: str, offset: integer, data: Type4.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
5086
+ compact: rpc(Type5.Object({ instructions: Type5.Optional(str) }, { additionalProperties: false }), nil),
5087
+ importImage: rpc(str, Type5.Object({ id: str, name: str })),
5088
+ draftImage: rpc(Type5.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
5089
+ beginImage: rpc(Type5.Object({ name: str, mimeType: str, size: Type5.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
5090
+ appendImage: rpc(Type5.Object({ id: str, offset: integer, data: Type5.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
4474
5091
  finishImage: rpc(str, nil),
4475
5092
  releaseImage: rpc(str, nil),
4476
- getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
5093
+ getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
4477
5094
  }
4478
5095
  }
4479
5096
  }
4480
5097
  });
4481
- browse = Type4.Object({ path: str, query: Type4.Optional(str), showHidden: Type4.Optional(bool), cursor: Type4.Optional(str) }, { additionalProperties: false });
4482
- page = Type4.Object({
5098
+ browse = Type5.Object({ path: str, query: Type5.Optional(str), showHidden: Type5.Optional(bool), cursor: Type5.Optional(str) }, { additionalProperties: false });
5099
+ page = Type5.Object({
4483
5100
  path: str,
4484
5101
  parent: nullableString,
4485
- entries: Type4.Array(Type4.Object({ name: str, path: str, error: Type4.Optional(str) })),
5102
+ entries: Type5.Array(Type5.Object({ name: str, path: str, error: Type5.Optional(str) })),
4486
5103
  next: nullableString
4487
5104
  });
4488
5105
  NekoWorkspaces = contract("neko.workspaces", {
4489
5106
  version: 1,
4490
- input: Type4.Object({}, { additionalProperties: false }),
5107
+ input: Type5.Object({}, { additionalProperties: false }),
4491
5108
  variants: {
4492
5109
  ready: {
4493
5110
  state: { state: WorkspaceStateSchema },
4494
5111
  methods: {
4495
- add: rpc(Type4.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
5112
+ add: rpc(Type5.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
4496
5113
  browse: rpc(browse, page)
4497
5114
  }
4498
5115
  }
4499
5116
  }
4500
5117
  });
4501
- provider = Type4.Enum(["deepseek", "tavily"]);
5118
+ provider = Type5.Enum(["deepseek", "tavily"]);
4502
5119
  NekoCredentials = contract("neko.credentials", {
4503
5120
  version: 1,
4504
- input: Type4.Object({}, { additionalProperties: false }),
5121
+ input: Type5.Object({}, { additionalProperties: false }),
4505
5122
  variants: {
4506
5123
  ready: {
4507
5124
  state: { state: CredentialStateSchema },
4508
5125
  methods: {
4509
- save: rpc(Type4.Object({ provider, key: Type4.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
4510
- remove: rpc(Type4.Object({ provider }, { additionalProperties: false }), nil)
5126
+ save: rpc(Type5.Object({ provider, key: Type5.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
5127
+ remove: rpc(Type5.Object({ provider }, { additionalProperties: false }), nil)
4511
5128
  }
4512
5129
  }
4513
5130
  }
4514
5131
  });
4515
- FileCandidatesSchema = Type4.Object({
4516
- entries: Type4.Array(Type4.Object({ name: str, path: str, kind: Type4.Enum(["file", "directory"]) }), { maxItems: 100 }),
5132
+ FileCandidatesSchema = Type5.Object({
5133
+ entries: Type5.Array(Type5.Object({ name: str, path: str, kind: Type5.Enum(["file", "directory"]) }), { maxItems: 100 }),
4517
5134
  limited: bool,
4518
5135
  incomplete: bool
4519
5136
  });
4520
5137
  NekoWorkspaceFiles = contract("neko.workspace-files", {
4521
5138
  version: 1,
4522
- input: Type4.Object({}, { additionalProperties: false }),
5139
+ input: Type5.Object({}, { additionalProperties: false }),
4523
5140
  variants: {
4524
5141
  ready: {
4525
5142
  methods: {
4526
- search: rpc(Type4.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
5143
+ search: rpc(Type5.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
4527
5144
  }
4528
5145
  }
4529
5146
  }
4530
5147
  });
4531
- marketSkill = Type4.Object({
5148
+ marketSkill = Type5.Object({
4532
5149
  name: str,
4533
5150
  description: str,
4534
5151
  category: str,
@@ -4538,38 +5155,38 @@ var init_contract = __esm(() => {
4538
5155
  sha256: str,
4539
5156
  sizeBytes: integer,
4540
5157
  uploadedAt: str,
4541
- installed: Type4.Object({ project: bool, user: bool, builtin: bool }),
4542
- activeLevel: Type4.Union([level, nil])
5158
+ installed: Type5.Object({ project: bool, user: bool, builtin: bool }),
5159
+ activeLevel: Type5.Union([level, nil])
4543
5160
  });
4544
- preview = Type4.Object({ body: str, files: Type4.Array(str) });
4545
- scope = Type4.Enum(["user", "project"]);
4546
- installResult = Type4.Object({ name: str, scope, path: str, shadowedBy: Type4.Optional(scope) });
5161
+ preview = Type5.Object({ body: str, files: Type5.Array(str) });
5162
+ scope = Type5.Enum(["user", "project"]);
5163
+ installResult = Type5.Object({ name: str, scope, path: str, shadowedBy: Type5.Optional(scope) });
4547
5164
  NekoSkillMarket = contract("neko.skill-market", {
4548
5165
  version: 1,
4549
- input: Type4.Object({}, { additionalProperties: false }),
5166
+ input: Type5.Object({}, { additionalProperties: false }),
4550
5167
  variants: {
4551
5168
  ready: {
4552
5169
  methods: {
4553
- list: rpc(Type4.Object({ query: Type4.Optional(str), workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), Type4.Array(marketSkill)),
4554
- detail: rpc(Type4.Object({ name: str, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), marketSkill),
4555
- preview: rpc(Type4.Object({ name: str }, { additionalProperties: false }), preview),
4556
- install: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str), overwrite: Type4.Optional(bool) }, { additionalProperties: false }), installResult),
4557
- uninstall: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), nil)
5170
+ list: rpc(Type5.Object({ query: Type5.Optional(str), workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), Type5.Array(marketSkill)),
5171
+ detail: rpc(Type5.Object({ name: str, workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), marketSkill),
5172
+ preview: rpc(Type5.Object({ name: str }, { additionalProperties: false }), preview),
5173
+ install: rpc(Type5.Object({ name: str, scope, workspaceRoot: Type5.Optional(str), overwrite: Type5.Optional(bool) }, { additionalProperties: false }), installResult),
5174
+ uninstall: rpc(Type5.Object({ name: str, scope, workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), nil)
4558
5175
  }
4559
5176
  }
4560
5177
  }
4561
5178
  });
4562
- ApplicationInfoSchema = Type4.Object({
5179
+ ApplicationInfoSchema = Type5.Object({
4563
5180
  runningVersion: str,
4564
5181
  installedVersion: nullableString,
4565
- protocolVersion: Type4.Integer({ minimum: 1 }),
5182
+ protocolVersion: Type5.Integer({ minimum: 1 }),
4566
5183
  latestVersion: nullableString,
4567
- checkedAt: Type4.Union([integer, nil]),
4568
- checkState: Type4.Enum(["unknown", "checking", "ok", "failed", "development"])
5184
+ checkedAt: Type5.Union([integer, nil]),
5185
+ checkState: Type5.Enum(["unknown", "checking", "ok", "failed", "development"])
4569
5186
  });
4570
5187
  NekoApplicationInfo = contract("neko.application-info", {
4571
5188
  version: 1,
4572
- input: Type4.Object({}),
5189
+ input: Type5.Object({}),
4573
5190
  variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
4574
5191
  });
4575
5192
  });
@@ -4906,6 +5523,27 @@ function waitForValue(state, signal) {
4906
5523
  abort();
4907
5524
  });
4908
5525
  }
5526
+ function jobsClient(chat) {
5527
+ const supported = () => chat.view.value?.jobs !== undefined;
5528
+ const require2 = () => {
5529
+ if (!supported())
5530
+ throw new Error(JOBS_UNSUPPORTED);
5531
+ };
5532
+ return {
5533
+ get supported() {
5534
+ return supported();
5535
+ },
5536
+ list: () => chat.view.value?.jobs ?? [],
5537
+ async output(jobId, since) {
5538
+ require2();
5539
+ return chat.jobOutput({ jobId, ...since === undefined ? {} : { since } });
5540
+ },
5541
+ async stop(jobId) {
5542
+ require2();
5543
+ return chat.jobStop({ jobId });
5544
+ }
5545
+ };
5546
+ }
4909
5547
  function createGatewayConnection(link, serverId) {
4910
5548
  const host = createHost({ kind: "client", upstream: identifiedLink(link, serverId), onError: () => {} });
4911
5549
  const get = (type) => host.get(type, {}, { at: serverId });
@@ -5058,6 +5696,7 @@ function deadline(work, signal) {
5058
5696
  signal?.removeEventListener("abort", abort);
5059
5697
  });
5060
5698
  }
5699
+ var JOBS_UNSUPPORTED = "This gateway does not support background jobs. Restart the gateway to enable them.";
5061
5700
  var init_client_session = __esm(() => {
5062
5701
  init_application_version();
5063
5702
  init_contract();
@@ -17240,6 +17879,7 @@ var init_labels = __esm(() => {
17240
17879
  { name: "login", description: "Configure DeepSeek API key" },
17241
17880
  { name: "model", description: "Choose a model" },
17242
17881
  { name: "agents", description: "View agent tasks and activity" },
17882
+ { name: "jobs", description: "View background jobs" },
17243
17883
  { name: "theme", description: "Choose color theme (auto/light/dark)" },
17244
17884
  { name: "think", description: "Set thinking level (/think high)" },
17245
17885
  { name: "fork", description: "Fork this session" },
@@ -17331,6 +17971,7 @@ class StatusFooter {
17331
17971
  const gap = Math.max(1, space - leftWidth - statusWidth);
17332
17972
  const first = ` ${left}${" ".repeat(gap)}${statusStyled}`;
17333
17973
  const agentHint = s.subagentCount ? this.panelOpen ? "" : `\u2192 Agents ${s.subagentCount}${s.approvalCount ? ` \xB7 Approval ${s.approvalCount}` : ""}` : "";
17974
+ const jobHint = s.jobCount && !this.panelOpen ? `Jobs ${s.jobCount}` : "";
17334
17975
  const hints = this.panelOpen ? "" : s.canAbort && !s.busy ? "Esc Stop" : s.busy ? labels.busyHints : labels.idleHints;
17335
17976
  const compactHints = s.canAbort && !s.busy ? "Esc Stop" : s.busy ? labels.busyCompact : labels.idleCompact;
17336
17977
  const context = s.contextUsage;
@@ -17341,7 +17982,7 @@ class StatusFooter {
17341
17982
  const shortContext = contextText.split("/")[0] ?? "";
17342
17983
  const shortEssentials = [shortContext, cost].filter(Boolean).join(" \xB7 ");
17343
17984
  const minimalHints = this.panelOpen ? "" : s.canAbort || s.busy ? "Esc Stop" : "/help Keys";
17344
- const joinHints = (metric, hint) => [metric, agentHint, hint].filter(Boolean).join(" \xB7 ");
17985
+ const joinHints = (metric, hint) => [metric, agentHint, jobHint, hint].filter(Boolean).join(" \xB7 ");
17345
17986
  const candidates = this.panelOpen ? [joinHints(essentials, hints), joinHints(shortEssentials, hints), joinHints("", hints)] : [
17346
17987
  joinHints(usage, compactHints),
17347
17988
  joinHints(essentials, compactHints),
@@ -17349,7 +17990,7 @@ class StatusFooter {
17349
17990
  joinHints(shortEssentials, minimalHints),
17350
17991
  joinHints("", minimalHints)
17351
17992
  ];
17352
- const second = candidates.find((line) => visibleWidth2(line) <= space) ?? (agentHint || minimalHints);
17993
+ const second = candidates.find((line) => visibleWidth2(line) <= space) ?? ([agentHint, jobHint].filter(Boolean).join(" \xB7 ") || minimalHints);
17353
17994
  const shownContext = second.includes(contextText) ? contextText : shortContext;
17354
17995
  const percent = context?.percent ?? null;
17355
17996
  const contextLabel = percent !== null && percent > 90 ? theme.error(shownContext) : percent !== null && percent > 75 ? theme.warning(shownContext) : theme.muted(shownContext);
@@ -17521,12 +18162,337 @@ var init_history_image = __esm(() => {
17521
18162
  };
17522
18163
  });
17523
18164
 
18165
+ // packages/core/src/job-summary.ts
18166
+ function formatDuration2(ms) {
18167
+ const total = Math.max(0, Math.floor(ms / 1000));
18168
+ if (total < 60)
18169
+ return `${total}s`;
18170
+ const minutes = Math.floor(total / 60);
18171
+ if (total < 3600)
18172
+ return `${minutes}m ${total % 60}s`;
18173
+ const hours = Math.floor(total / 3600);
18174
+ if (total < 86400)
18175
+ return `${hours}h ${minutes % 60}m`;
18176
+ return `${Math.floor(total / 86400)}d ${hours % 24}h`;
18177
+ }
18178
+ function jobLabel(job) {
18179
+ return job.title.trim() || firstLine(job.command) || "(empty command)";
18180
+ }
18181
+ function jobMeta(job, now) {
18182
+ if (job.status === "running")
18183
+ return formatDuration2(now - job.startedAt);
18184
+ if (job.reason === "timeout")
18185
+ return "timeout";
18186
+ if (job.exitCode !== null)
18187
+ return `exit ${job.exitCode}`;
18188
+ if (job.reason === "restart")
18189
+ return "restart";
18190
+ return formatDuration2((job.endedAt ?? now) - job.startedAt);
18191
+ }
18192
+ function summarizeJobs(jobs, now) {
18193
+ const rows = [...jobs].sort((a, b) => b.startedAt - a.startedAt || jobNumber(b.jobId) - jobNumber(a.jobId)).map((job) => {
18194
+ const running = job.status === "running";
18195
+ return {
18196
+ job,
18197
+ label: jobLabel(job),
18198
+ running,
18199
+ statusText: job.status,
18200
+ meta: jobMeta(job, now),
18201
+ durationMs: Math.max(0, (job.endedAt ?? now) - job.startedAt)
18202
+ };
18203
+ });
18204
+ return { rows, running: rows.filter((row) => row.running).length };
18205
+ }
18206
+ function splitJobReport(text) {
18207
+ const lines = text.split(`
18208
+ `);
18209
+ const head = lines[0] ?? "";
18210
+ const command = lines[1]?.startsWith("Command: ") ? lines[1].slice("Command: ".length) : undefined;
18211
+ const at = lines.findIndex((line, i) => i > 0 && line.startsWith("Last output ("));
18212
+ const output = at < 0 ? undefined : lines.slice(at + 1).join(`
18213
+ `);
18214
+ return { head, command, output: output === "" ? undefined : output };
18215
+ }
18216
+
18217
+ class JobTail {
18218
+ #text = "";
18219
+ #dropped = 0;
18220
+ #offset;
18221
+ #keep;
18222
+ constructor(keepChars = TAIL_KEEP_CHARS) {
18223
+ this.#keep = keepChars;
18224
+ }
18225
+ get text() {
18226
+ return this.#text;
18227
+ }
18228
+ get dropped() {
18229
+ return this.#dropped;
18230
+ }
18231
+ get offset() {
18232
+ return this.#offset;
18233
+ }
18234
+ apply(read, transform = (text) => text) {
18235
+ if (this.#offset === undefined)
18236
+ this.#dropped = read.dropped;
18237
+ else if (read.truncated)
18238
+ this.#dropped += read.dropped;
18239
+ this.#offset = read.offset;
18240
+ this.#text += transform(read.text);
18241
+ if (this.#text.length > this.#keep) {
18242
+ const cut = this.#text.length - this.#keep;
18243
+ this.#text = this.#text.slice(cut);
18244
+ this.#dropped += cut;
18245
+ }
18246
+ }
18247
+ }
18248
+ var firstLine = (text) => text.split(`
18249
+ `).map((line) => line.trim()).find(Boolean) ?? "", jobNumber = (jobId) => Number.parseInt(jobId.replace(/^\D+/, ""), 10) || 0, TAIL_KEEP_CHARS;
18250
+ var init_job_summary = __esm(() => {
18251
+ TAIL_KEEP_CHARS = 256 * 1024;
18252
+ });
18253
+
18254
+ // packages/tui/src/jobs-ui.ts
18255
+ import { matchesKey as matchesKey4, stripTerminalSequences as stripTerminalSequences2, wrapTextWithAnsi as wrapTextWithAnsi2 } from "@earendil-works/pi-tui";
18256
+ function jobItems(jobs, now = Date.now()) {
18257
+ return summarizeJobs(jobs, now).rows.map(({ job, label, running, statusText, meta }) => ({
18258
+ value: job.jobId,
18259
+ label: `${job.jobId} \xB7 ${oneLine2(label, 80)}`,
18260
+ description: `${oneLine2(job.command)} \xB7 ${job.cwd}`,
18261
+ rightLabel: running ? `${statusText} \xB7 ${meta}` : meta === statusText ? statusText : `${statusText} \xB7 ${meta}`
18262
+ }));
18263
+ }
18264
+ function sanitizeOutput(text) {
18265
+ const plain = stripTerminalSequences2(text).replace(/\r\n?/g, `
18266
+ `).replace(/\t/g, " ");
18267
+ let out = "";
18268
+ for (const char of plain)
18269
+ if (!isControl(char.charCodeAt(0)))
18270
+ out += char;
18271
+ return out;
18272
+ }
18273
+ function confirmStop(panel, jobs, jobId, onError, changed) {
18274
+ let submitted = false;
18275
+ panel.showDetail({
18276
+ help: shortcutHints(["Enter", "Stop"], ["Esc", "Back"]),
18277
+ lines: () => [theme.warning(`Stop job ${jobId}? Its process tree is terminated.`)],
18278
+ handleKey: (key) => {
18279
+ if (!matchesKey4(key, "enter"))
18280
+ return false;
18281
+ if (submitted)
18282
+ return true;
18283
+ submitted = true;
18284
+ jobs.stop(jobId).catch((error) => onError(error instanceof Error ? error.message : String(error))).finally(() => {
18285
+ panel.popDetail();
18286
+ changed();
18287
+ });
18288
+ return true;
18289
+ }
18290
+ });
18291
+ }
18292
+
18293
+ class JobDetail {
18294
+ jobId;
18295
+ #jobs;
18296
+ #panel;
18297
+ #changed;
18298
+ #tail = new JobTail;
18299
+ #scroll = 0;
18300
+ #maxScroll = 0;
18301
+ #page = 10;
18302
+ #follow = true;
18303
+ #error = "";
18304
+ #active = false;
18305
+ #epoch = 0;
18306
+ #timer;
18307
+ #polling = false;
18308
+ constructor(jobs, jobId, panel, changed) {
18309
+ this.#jobs = jobs;
18310
+ this.jobId = jobId;
18311
+ this.#panel = panel;
18312
+ this.#changed = changed;
18313
+ }
18314
+ get #job() {
18315
+ return this.#jobs.list().find((job) => job.jobId === this.jobId);
18316
+ }
18317
+ get help() {
18318
+ const running = this.#job?.status === "running";
18319
+ return shortcutHints(["\u2190", "Back"], ["\u2191\u2193/Wheel", "Scroll"], ...this.#follow ? [] : [["End", "Bottom"]], ...running ? [["S", "Stop"]] : []);
18320
+ }
18321
+ open() {
18322
+ if (this.#active)
18323
+ return this;
18324
+ this.#active = true;
18325
+ this.#epoch++;
18326
+ this.#poll();
18327
+ return this;
18328
+ }
18329
+ close() {
18330
+ this.#active = false;
18331
+ this.#epoch++;
18332
+ clearTimeout(this.#timer);
18333
+ this.#timer = undefined;
18334
+ }
18335
+ async#poll() {
18336
+ if (!this.#active || this.#polling)
18337
+ return;
18338
+ this.#polling = true;
18339
+ const epoch = this.#epoch;
18340
+ const wasRunning = this.#job?.status === "running";
18341
+ try {
18342
+ const read = await this.#jobs.output(this.jobId, this.#tail.offset);
18343
+ if (epoch !== this.#epoch)
18344
+ return;
18345
+ this.#error = "";
18346
+ this.#tail.apply(read, sanitizeOutput);
18347
+ this.#schedule(wasRunning);
18348
+ } catch (error) {
18349
+ if (epoch !== this.#epoch)
18350
+ return;
18351
+ this.#error = error instanceof Error ? error.message : String(error);
18352
+ } finally {
18353
+ this.#polling = false;
18354
+ if (epoch === this.#epoch)
18355
+ this.#changed();
18356
+ }
18357
+ }
18358
+ #schedule(keepGoing) {
18359
+ clearTimeout(this.#timer);
18360
+ this.#timer = undefined;
18361
+ if (!this.#active || !keepGoing)
18362
+ return;
18363
+ this.#timer = setTimeout(() => void this.#poll(), POLL_MS);
18364
+ }
18365
+ scroll(delta) {
18366
+ this.#scroll = Math.max(0, Math.min(this.#maxScroll, this.#scroll + delta));
18367
+ this.#follow = delta > 0 && this.#scroll === this.#maxScroll;
18368
+ }
18369
+ handleKey(data) {
18370
+ if (matchesKey4(data, "left"))
18371
+ this.#panel.popDetail();
18372
+ else if (matchesKey4(data, "up"))
18373
+ this.scroll(-1);
18374
+ else if (matchesKey4(data, "down"))
18375
+ this.scroll(1);
18376
+ else if (matchesKey4(data, "pageUp"))
18377
+ this.scroll(-this.#page);
18378
+ else if (matchesKey4(data, "pageDown"))
18379
+ this.scroll(this.#page);
18380
+ else if (matchesKey4(data, "end")) {
18381
+ this.#follow = true;
18382
+ this.#scroll = this.#maxScroll;
18383
+ } else if (data.toLowerCase() === "s" && this.#job?.status === "running")
18384
+ confirmStop(this.#panel, this.#jobs, this.jobId, (message) => {
18385
+ this.#error = message;
18386
+ }, this.#changed);
18387
+ else
18388
+ return false;
18389
+ return true;
18390
+ }
18391
+ lines(width, height) {
18392
+ this.#page = Math.max(1, height - 1);
18393
+ const job = this.#job;
18394
+ const rows = [];
18395
+ if (!job) {
18396
+ rows.push(theme.dim("This job is no longer available."));
18397
+ } else {
18398
+ const row = summarizeJobs([job], Date.now()).rows[0];
18399
+ rows.push(theme.accent(`${job.jobId} \xB7 ${oneLine2(row.label, 120)}`), theme.dim(`${row.statusText} \xB7 ${row.meta}`));
18400
+ const command = wrapTextWithAnsi2(`$ ${job.command}`, width);
18401
+ rows.push(...command.slice(0, COMMAND_LINES).map((line) => theme.dim(line)));
18402
+ if (command.length > COMMAND_LINES)
18403
+ rows.push(theme.dim("\u2026"));
18404
+ rows.push(theme.dim(job.cwd), "");
18405
+ }
18406
+ if (this.#tail.dropped > 0)
18407
+ rows.push(theme.warning(`\u2026 ${this.#tail.dropped} bytes of earlier output were dropped`));
18408
+ const body = this.#tail.text.replace(/\n$/, "");
18409
+ if (body === "") {
18410
+ if (this.#tail.offset !== undefined && this.#error === "")
18411
+ rows.push(theme.dim("(no output yet)"));
18412
+ } else
18413
+ for (const line of body.split(`
18414
+ `))
18415
+ rows.push(...wrapTextWithAnsi2(line, width));
18416
+ if (this.#error)
18417
+ rows.push(theme.error(this.#error));
18418
+ this.#maxScroll = Math.max(0, rows.length - height);
18419
+ this.#scroll = this.#follow ? this.#maxScroll : Math.min(this.#scroll, this.#maxScroll);
18420
+ return rows.slice(this.#scroll, this.#scroll + height);
18421
+ }
18422
+ }
18423
+ var JOBS_EMPTY = "No background jobs in this session.", POLL_MS = 1000, COMMAND_LINES = 3, oneLine2 = (text, max = 200) => {
18424
+ const line = text.replace(/\s+/g, " ").trim();
18425
+ return line.length > max ? `${line.slice(0, max - 1)}\u2026` : line;
18426
+ }, isControl = (code) => code < 32 && code !== 10 || code === 127;
18427
+ var init_jobs_ui = __esm(() => {
18428
+ init_job_summary();
18429
+ init_shortcut_hints();
18430
+ init_theme();
18431
+ });
18432
+
18433
+ // packages/tui/src/components/job-report.ts
18434
+ import { Container as Container3, MouseRegion as MouseRegion2, Text as Text4 } from "@earendil-works/pi-tui";
18435
+ var MAX_LINES = 40, JobReportNotice;
18436
+ var init_job_report = __esm(() => {
18437
+ init_job_summary();
18438
+ init_jobs_ui();
18439
+ init_theme();
18440
+ JobReportNotice = class JobReportNotice extends Container3 {
18441
+ #raw;
18442
+ #expanded = false;
18443
+ #text = new Text4("", 1, 0);
18444
+ constructor(text) {
18445
+ super();
18446
+ this.#raw = text;
18447
+ this.addChild(new MouseRegion2(this.#text, (event) => this.#onClick(event)));
18448
+ }
18449
+ setText(text) {
18450
+ this.#raw = text;
18451
+ }
18452
+ setExpanded(expanded) {
18453
+ this.#expanded = expanded;
18454
+ }
18455
+ #onClick(event) {
18456
+ if (event.type !== "click" || event.button !== "left")
18457
+ return;
18458
+ this.#expanded = !this.#expanded;
18459
+ return { handled: true };
18460
+ }
18461
+ render(width) {
18462
+ const { head, command, output } = splitJobReport(this.#raw);
18463
+ let text = theme.dim(sanitizeOutput(head));
18464
+ if (output === undefined) {
18465
+ if (command !== undefined)
18466
+ text += theme.dim(`
18467
+ $ ${sanitizeOutput(command)}`);
18468
+ } else {
18469
+ const lines = sanitizeOutput(output).split(`
18470
+ `);
18471
+ if (this.#expanded) {
18472
+ if (command !== undefined)
18473
+ text += theme.dim(`
18474
+ $ ${sanitizeOutput(command)}`);
18475
+ text += `
18476
+ ${theme.toolOutput(lines.slice(-MAX_LINES).join(`
18477
+ `))}`;
18478
+ if (lines.length > MAX_LINES)
18479
+ text += theme.dim(`
18480
+ \u2026 ${lines.length - MAX_LINES} earlier lines omitted`);
18481
+ } else
18482
+ text += theme.dim(` +${lines.length} lines`);
18483
+ }
18484
+ this.#text.setText(text);
18485
+ return super.render(width);
18486
+ }
18487
+ };
18488
+ });
18489
+
17524
18490
  // packages/tui/src/components/login.ts
17525
18491
  import {
17526
18492
  CURSOR_MARKER as CURSOR_MARKER2,
17527
18493
  Input,
17528
18494
  isKeyRelease as isKeyRelease2,
17529
- matchesKey as matchesKey4,
18495
+ matchesKey as matchesKey5,
17530
18496
  renderFakeCursor,
17531
18497
  truncateToWidth as truncateToWidth3
17532
18498
  } from "@earendil-works/pi-tui";
@@ -17590,7 +18556,7 @@ class LoginPanel {
17590
18556
  handleInput(data) {
17591
18557
  if (isKeyRelease2(data))
17592
18558
  return;
17593
- if (matchesKey4(data, "escape") || matchesKey4(data, "ctrl+c")) {
18559
+ if (matchesKey5(data, "escape") || matchesKey5(data, "ctrl+c")) {
17594
18560
  this.close();
17595
18561
  return;
17596
18562
  }
@@ -17598,18 +18564,18 @@ class LoginPanel {
17598
18564
  return;
17599
18565
  const confirming = this.#confirmingRemove;
17600
18566
  this.#confirmingRemove = false;
17601
- if (confirming && !matchesKey4(data, "ctrl+r"))
18567
+ if (confirming && !matchesKey5(data, "ctrl+r"))
17602
18568
  this.#message = "";
17603
- if (matchesKey4(data, "enter"))
18569
+ if (matchesKey5(data, "enter"))
17604
18570
  this.#apply(false);
17605
- else if (matchesKey4(data, "ctrl+r") && this.#status?.savedConfigured) {
18571
+ else if (matchesKey5(data, "ctrl+r") && this.#status?.savedConfigured) {
17606
18572
  if (confirming)
17607
18573
  this.#apply(true);
17608
18574
  else {
17609
18575
  this.#confirmingRemove = true;
17610
18576
  this.#message = "Press Ctrl+R again to remove the saved key.";
17611
18577
  }
17612
- } else if (matchesKey4(data, "tab") || (matchesKey4(data, "left") || matchesKey4(data, "right")) && this.#input.getValue() === "")
18578
+ } else if (matchesKey5(data, "tab") || (matchesKey5(data, "left") || matchesKey5(data, "right")) && this.#input.getValue() === "")
17613
18579
  this.#switch();
17614
18580
  else {
17615
18581
  this.#input.handleInput(data);
@@ -17727,15 +18693,17 @@ var init_notice = __esm(() => {
17727
18693
  import {
17728
18694
  Input as Input2,
17729
18695
  isKeyRelease as isKeyRelease3,
17730
- matchesKey as matchesKey5,
18696
+ matchesKey as matchesKey6,
17731
18697
  truncateToWidth as truncateToWidth4,
17732
18698
  visibleWidth as visibleWidth3,
17733
- wrapTextWithAnsi as wrapTextWithAnsi2
18699
+ wrapTextWithAnsi as wrapTextWithAnsi3
17734
18700
  } from "@earendil-works/pi-tui";
17735
18701
 
17736
18702
  class PickerPanel {
17737
18703
  pointerSelection = false;
17738
18704
  agentNavigation = false;
18705
+ emptyText;
18706
+ listKey;
17739
18707
  search = new Input2({ prompt: "\u276F ", placeholder: "Type to filter" });
17740
18708
  #items = [];
17741
18709
  #index = 0;
@@ -17804,6 +18772,8 @@ class PickerPanel {
17804
18772
  close() {
17805
18773
  this.pointerSelection = false;
17806
18774
  this.agentNavigation = false;
18775
+ this.emptyText = undefined;
18776
+ this.listKey = undefined;
17807
18777
  ++this.#generation;
17808
18778
  for (const detail of this.#details)
17809
18779
  detail.close?.();
@@ -17855,14 +18825,14 @@ class PickerPanel {
17855
18825
  handleInput(data) {
17856
18826
  if (isKeyRelease3(data))
17857
18827
  return;
17858
- if (this.agentNavigation && matchesKey5(data, "left")) {
18828
+ if (this.agentNavigation && matchesKey6(data, "left")) {
17859
18829
  this.close();
17860
18830
  return;
17861
18831
  }
17862
- if (this.agentNavigation && !this.detailOpen && this.search.getValue() === "" && matchesKey5(data, "right"))
18832
+ if (this.agentNavigation && !this.detailOpen && this.search.getValue() === "" && matchesKey6(data, "right"))
17863
18833
  data = "\r";
17864
18834
  if (this.#detail !== undefined) {
17865
- if (matchesKey5(data, "escape") || matchesKey5(data, "ctrl+c")) {
18835
+ if (matchesKey6(data, "escape") || matchesKey6(data, "ctrl+c")) {
17866
18836
  if (this.#detail.backCloses)
17867
18837
  this.close();
17868
18838
  else
@@ -17872,23 +18842,27 @@ class PickerPanel {
17872
18842
  }
17873
18843
  return;
17874
18844
  }
17875
- if (matchesKey5(data, "escape") || matchesKey5(data, "ctrl+c")) {
18845
+ if (matchesKey6(data, "escape") || matchesKey6(data, "ctrl+c")) {
17876
18846
  this.close();
17877
18847
  return;
17878
18848
  }
17879
18849
  if (this.#state === "error") {
17880
- if (matchesKey5(data, "enter"))
18850
+ if (matchesKey6(data, "enter"))
17881
18851
  this.reload();
17882
18852
  return;
17883
18853
  }
17884
18854
  if (this.#state !== "ready")
17885
18855
  return;
17886
18856
  const items = this.#filtered();
17887
- if (matchesKey5(data, "up"))
18857
+ if (this.listKey?.(data, items[this.#index]?.value)) {
18858
+ this.changed();
18859
+ return;
18860
+ }
18861
+ if (matchesKey6(data, "up"))
17888
18862
  this.#move(-1);
17889
- else if (matchesKey5(data, "down"))
18863
+ else if (matchesKey6(data, "down"))
17890
18864
  this.#move(1);
17891
- else if (matchesKey5(data, "enter")) {
18865
+ else if (matchesKey6(data, "enter")) {
17892
18866
  const item = items[this.#index];
17893
18867
  if (!item || item.header)
17894
18868
  return;
@@ -17961,7 +18935,7 @@ class PickerPanel {
17961
18935
  `${theme.accent(style.bold(this.#title))}${this.#state === "ready" && !this.#detail?.backCloses ? theme.muted(` (${this.#items.length})`) : ""}`
17962
18936
  ];
17963
18937
  if (this.#detail !== undefined) {
17964
- const hints = wrapTextWithAnsi2(this.#detail.help, w).slice(0, Math.max(1, height - 2));
18938
+ const hints = wrapTextWithAnsi3(this.#detail.help, w).slice(0, Math.max(1, height - 2));
17965
18939
  const body = this.#detail.lines(w, Math.max(1, height - 1 - hints.length));
17966
18940
  rows.push(...body);
17967
18941
  while (rows.length < height - hints.length)
@@ -17985,7 +18959,7 @@ class PickerPanel {
17985
18959
  this.#index = first;
17986
18960
  }
17987
18961
  const selected = items[this.#index];
17988
- const details = height >= 9 && selected?.description ? wrapTextWithAnsi2(selected.description, w).slice(0, 2) : [];
18962
+ const details = height >= 9 && selected?.description ? wrapTextWithAnsi3(selected.description, w).slice(0, 2) : [];
17989
18963
  const count = Math.max(1, available - details.length);
17990
18964
  const start = Math.max(0, Math.min(this.#index - Math.floor(count / 2), items.length - count));
17991
18965
  for (const [i, item] of items.slice(start, start + count).entries()) {
@@ -18003,7 +18977,7 @@ class PickerPanel {
18003
18977
  rows.push(rendered);
18004
18978
  }
18005
18979
  if (items.length === 0)
18006
- rows.push(labels.noResults);
18980
+ rows.push(this.emptyText !== undefined && this.#items.length === 0 ? this.emptyText : labels.noResults);
18007
18981
  while (rows.length < height - 1 - details.length)
18008
18982
  rows.push("");
18009
18983
  rows.push(...details.map((line) => theme.dim(line)));
@@ -18022,7 +18996,7 @@ var init_picker = __esm(() => {
18022
18996
  });
18023
18997
 
18024
18998
  // packages/tui/src/components/themed-text.ts
18025
- import { Text as Text4, TruncatedText } from "@earendil-works/pi-tui";
18999
+ import { Text as Text5, TruncatedText } from "@earendil-works/pi-tui";
18026
19000
 
18027
19001
  class ThemedTruncatedText {
18028
19002
  #content;
@@ -18046,12 +19020,12 @@ class ThemedWrappedText {
18046
19020
  }
18047
19021
  invalidate() {}
18048
19022
  render(width) {
18049
- return new Text4(this.#content(), 1, 0).render(width);
19023
+ return new Text5(this.#content(), 1, 0).render(width);
18050
19024
  }
18051
19025
  }
18052
19026
  var ThemedText;
18053
19027
  var init_themed_text = __esm(() => {
18054
- ThemedText = class ThemedText extends Text4 {
19028
+ ThemedText = class ThemedText extends Text5 {
18055
19029
  #raw;
18056
19030
  #color;
18057
19031
  constructor(text, color, paddingX = 1, paddingY = 0) {
@@ -18168,7 +19142,7 @@ var init_diff_view = __esm(() => {
18168
19142
  });
18169
19143
 
18170
19144
  // packages/tui/src/components/tool-execution.ts
18171
- import { Box, Container as Container3, MouseRegion as MouseRegion2, Text as Text5 } from "@earendil-works/pi-tui";
19145
+ import { Box, Container as Container4, MouseRegion as MouseRegion3, Text as Text6 } from "@earendil-works/pi-tui";
18172
19146
  function currentToolFrame() {
18173
19147
  return SPINNER_FRAMES[spinnerFrame];
18174
19148
  }
@@ -18215,7 +19189,7 @@ var init_tool_execution = __esm(() => {
18215
19189
  init_syntax();
18216
19190
  init_diff_view();
18217
19191
  SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
18218
- ToolExecutionComponent = class ToolExecutionComponent extends Container3 {
19192
+ ToolExecutionComponent = class ToolExecutionComponent extends Container4 {
18219
19193
  #renderTheme = getActiveThemeMode();
18220
19194
  #state;
18221
19195
  #expanded = false;
@@ -18225,9 +19199,9 @@ var init_tool_execution = __esm(() => {
18225
19199
  constructor(state) {
18226
19200
  super();
18227
19201
  this.#state = state;
18228
- this.#text = new Text5("", 0, 0);
19202
+ this.#text = new Text6("", 0, 0);
18229
19203
  this.#box = new Box(1, 0, theme.toolPendingBg);
18230
- this.#box.addChild(new MouseRegion2(this.#text, (event) => this.#onClick(event)));
19204
+ this.#box.addChild(new MouseRegion3(this.#text, (event) => this.#onClick(event)));
18231
19205
  this.addChild(this.#box);
18232
19206
  this.rebuild();
18233
19207
  }
@@ -18320,11 +19294,11 @@ ${theme.error(state.note)}`;
18320
19294
  });
18321
19295
 
18322
19296
  // packages/tui/src/components/user-message.ts
18323
- import { Container as Container4, Markdown as Markdown2, MouseRegion as MouseRegion3, Text as Text6 } from "@earendil-works/pi-tui";
19297
+ import { Container as Container5, Markdown as Markdown2, MouseRegion as MouseRegion4, Text as Text7 } from "@earendil-works/pi-tui";
18324
19298
  var UserMessageComponent;
18325
19299
  var init_user_message = __esm(() => {
18326
19300
  init_theme();
18327
- UserMessageComponent = class UserMessageComponent extends Container4 {
19301
+ UserMessageComponent = class UserMessageComponent extends Container5 {
18328
19302
  #renderTheme = getActiveThemeMode();
18329
19303
  #text;
18330
19304
  #skill;
@@ -18357,14 +19331,14 @@ var init_user_message = __esm(() => {
18357
19331
  this.clear();
18358
19332
  const prompt = this.#defaultImagePrompt;
18359
19333
  if (prompt) {
18360
- this.addChild(new MouseRegion3(new Text6(theme.dim(`${this.#expanded ? "v" : ">"} Default image prompt \xB7 Click to ${this.#expanded ? "hide" : "view"}`), 1, 0, theme.userBg), (event) => {
19334
+ this.addChild(new MouseRegion4(new Text7(theme.dim(`${this.#expanded ? "v" : ">"} Default image prompt \xB7 Click to ${this.#expanded ? "hide" : "view"}`), 1, 0, theme.userBg), (event) => {
18361
19335
  if (event.type !== "click" || event.button !== "left")
18362
19336
  return;
18363
19337
  this.setExpanded(!this.#expanded);
18364
19338
  return { handled: true };
18365
19339
  }));
18366
19340
  if (this.#expanded)
18367
- this.addChild(new Text6(prompt.text, 1, 0, theme.userBg));
19341
+ this.addChild(new Text7(prompt.text, 1, 0, theme.userBg));
18368
19342
  if (!this.#text.trim())
18369
19343
  return;
18370
19344
  }
@@ -18374,7 +19348,7 @@ var init_user_message = __esm(() => {
18374
19348
  const preview = args.length > 100 ? `${args.slice(0, 99)}\u2026` : args;
18375
19349
  const head = `${theme.toolTitle(`\u2726 skill:${invocation.name}`)}${preview ? ` ${theme.userText(preview)}` : ""}`;
18376
19350
  const source = theme.dim(`skill body from ${invocation.location} (collapsed)`);
18377
- this.addChild(new Text6(`${head}
19351
+ this.addChild(new Text7(`${head}
18378
19352
  ${source}`, 1, 0, theme.userBg));
18379
19353
  return;
18380
19354
  }
@@ -19037,7 +20011,7 @@ import {
19037
20011
  resource
19038
20012
  } from "@earendil-works/chord";
19039
20013
  import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
19040
- import { Type as Type5 } from "typebox";
20014
+ import { Type as Type6 } from "typebox";
19041
20015
  async function openReader(runtime, childId, parentId, context, through, historyUsage) {
19042
20016
  const child = await runtime.harness.conversation(childId, context);
19043
20017
  if (!child)
@@ -19162,7 +20136,7 @@ async function openReader(runtime, childId, parentId, context, through, historyU
19162
20136
  }
19163
20137
  function installSubagentHost(host, runtime, context) {
19164
20138
  host.serve(ChildProjection, (input, _principal, self) => {
19165
- const { revision } = self.state({ revision: cell(Type5.Integer(), 0) });
20139
+ const { revision } = self.state({ revision: cell(Type6.Integer(), 0) });
19166
20140
  const { reader } = self.resources({
19167
20141
  reader: resource({
19168
20142
  rev: 1,
@@ -19204,7 +20178,7 @@ function installSubagentHost(host, runtime, context) {
19204
20178
  };
19205
20179
  const { target } = self.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
19206
20180
  const shared = self.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
19207
- const { authorizedRevision } = self.state({ authorizedRevision: cell(Type5.Integer(), 0) });
20181
+ const { authorizedRevision } = self.state({ authorizedRevision: cell(Type6.Integer(), 0) });
19208
20182
  self.listen({
19209
20183
  authorizedView: listener(shared.revision, async (value, ctx) => {
19210
20184
  try {
@@ -19265,9 +20239,9 @@ var init_subagent_host = __esm(() => {
19265
20239
  init_session_host();
19266
20240
  ChildProjection = local("neko.subagent-projection", {
19267
20241
  version: 1,
19268
- input: Type5.Object({ parentId: Type5.Integer(), agentId: Type5.String() }),
20242
+ input: Type6.Object({ parentId: Type6.Integer(), agentId: Type6.String() }),
19269
20243
  members: {
19270
- revision: Type5.Integer(),
20244
+ revision: Type6.Integer(),
19271
20245
  reader: opaque("neko.subagent-reader@1")
19272
20246
  }
19273
20247
  });
@@ -19285,9 +20259,9 @@ import {
19285
20259
  opaque as opaque2,
19286
20260
  resource as resource2
19287
20261
  } from "@earendil-works/chord";
19288
- import { withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
20262
+ import { withAbortSignal as withAbortSignal4 } from "@earendil-works/chord/context";
19289
20263
  import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
19290
- import { Type as Type6 } from "typebox";
20264
+ import { Type as Type7 } from "typebox";
19291
20265
  async function openProjection(runtime, session, context) {
19292
20266
  const view = await session.viewState();
19293
20267
  const history = new Map;
@@ -19303,7 +20277,7 @@ async function openProjection(runtime, session, context) {
19303
20277
  const stopMetadata = runtime.onDirectoryChange(() => changed?.(null));
19304
20278
  let childIds = new Set;
19305
20279
  const stopChildren = runtime.harness.subscribeCommits(({ changes }) => {
19306
- if (changes.some((change) => change.type === "document" && (["app.neko.delegations", "app.neko.approvals", "app.neko.subagents"].includes(change.record.kind) && change.conversationId === session.id || change.record.kind === "pi.usage" && change.conversationId !== undefined && childIds.has(change.conversationId))))
20280
+ if (changes.some((change) => change.type === "document" && (["app.neko.delegations", "app.neko.approvals", "app.neko.subagents", "app.neko.jobs"].includes(change.record.kind) && change.conversationId === session.id || change.record.kind === "pi.usage" && change.conversationId !== undefined && childIds.has(change.conversationId))))
19307
20281
  changed?.(null);
19308
20282
  });
19309
20283
  return {
@@ -19329,6 +20303,7 @@ async function openProjection(runtime, session, context) {
19329
20303
  }, naming)
19330
20304
  }
19331
20305
  }, "", { models: runtime.listModels(), contextEntries: view.value.entries, compaction });
20306
+ const jobs = (await session.jobs()).map(clipJob);
19332
20307
  const subagents = await listDelegations(runtime.harness, session.id, context);
19333
20308
  const approvals = await runtime.approvals.list(session.id, context);
19334
20309
  for (const entry of subagents)
@@ -19343,6 +20318,7 @@ async function openProjection(runtime, session, context) {
19343
20318
  const u = totalUsage(aggregate);
19344
20319
  return {
19345
20320
  ...projected,
20321
+ jobs,
19346
20322
  agents: summarizeSubagents(subagents, (await runtime.harness.snapshot(SubagentsDoc, session.id, context))?.items),
19347
20323
  subagents: subagents.map(({ usageStart: _start, usageEnd: _end, ...entry }) => ({
19348
20324
  ...entry,
@@ -19514,7 +20490,7 @@ function serveSessions(host, runtime, context) {
19514
20490
  getImage: async (input, ctx) => {
19515
20491
  if (!Number.isSafeInteger(input.offset) || input.offset < 0 || !Number.isSafeInteger(input.contentIndex) || input.contentIndex < 0)
19516
20492
  throw new ChordError2("invalid", "Invalid image range");
19517
- const entry = await shared.reader.entry(input.entryId, withAbortSignal3(ctx.signal, context));
20493
+ const entry = await shared.reader.entry(input.entryId, withAbortSignal4(ctx.signal, context));
19518
20494
  const content = entry?.model?.[0]?.content;
19519
20495
  const image = Array.isArray(content) ? content[input.contentIndex] : undefined;
19520
20496
  if (image?.type !== "image")
@@ -19551,6 +20527,21 @@ function serveSessions(host, runtime, context) {
19551
20527
  return null;
19552
20528
  },
19553
20529
  skills: () => runtime.listSkills(projection.value.session.id),
20530
+ jobOutput: async ({ jobId, since }) => {
20531
+ try {
20532
+ const { job, output } = await projection.value.session.jobOutput(jobId, since);
20533
+ return { job: clipJob(job), ...output };
20534
+ } catch (error) {
20535
+ throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot read job output");
20536
+ }
20537
+ },
20538
+ jobStop: async ({ jobId }) => {
20539
+ try {
20540
+ return clipJob(await projection.value.session.stopJob(jobId));
20541
+ } catch (error) {
20542
+ throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot stop job");
20543
+ }
20544
+ },
19554
20545
  usage: async () => {
19555
20546
  const usage = totalUsage(await projection.value.session.usage());
19556
20547
  return {
@@ -19586,14 +20577,14 @@ function serveSessions(host, runtime, context) {
19586
20577
  return behavior;
19587
20578
  });
19588
20579
  }
19589
- var OwnerInput, Owner, Projection, emptyView;
20580
+ var OwnerInput, Owner, Projection, emptyView, JOB_COMMAND_PREVIEW_CHARS = 1000, clipJob = (job) => job.command.length > JOB_COMMAND_PREVIEW_CHARS ? { ...job, command: `${job.command.slice(0, JOB_COMMAND_PREVIEW_CHARS)}\u2026` } : job;
19590
20581
  var init_session_host = __esm(() => {
19591
20582
  init_src();
19592
20583
  init_workspace_files();
19593
20584
  init_contract();
19594
20585
  init_image_uploads();
19595
20586
  init_subagent_host();
19596
- OwnerInput = Type6.Object({});
20587
+ OwnerInput = Type7.Object({});
19597
20588
  Owner = local2("neko.attachment-owner", {
19598
20589
  version: 1,
19599
20590
  input: OwnerInput,
@@ -19601,7 +20592,7 @@ var init_session_host = __esm(() => {
19601
20592
  });
19602
20593
  Projection = local2("neko.projection", {
19603
20594
  version: 1,
19604
- input: Type6.Object({ id: Type6.String() }),
20595
+ input: Type7.Object({ id: Type7.String() }),
19605
20596
  members: {
19606
20597
  view: ChatViewSchema,
19607
20598
  reader: opaque2("neko.projection-reader@1")
@@ -20300,7 +21291,7 @@ import {
20300
21291
  local as local3,
20301
21292
  method as method2
20302
21293
  } from "@earendil-works/chord";
20303
- import { Type as Type7 } from "typebox";
21294
+ import { Type as Type8 } from "typebox";
20304
21295
  async function createNekoServerServices(options) {
20305
21296
  const { runtime, context, host } = options;
20306
21297
  const updates = new UpdateChecker(options.updates);
@@ -20528,12 +21519,12 @@ var init_server_services = __esm(() => {
20528
21519
  init_workspaces();
20529
21520
  ServerState = local3("neko.server-state", {
20530
21521
  version: 1,
20531
- input: Type7.Object({}),
21522
+ input: Type8.Object({}),
20532
21523
  members: {
20533
21524
  directory: DirectoryStateSchema,
20534
21525
  workspaces: WorkspaceStateSchema,
20535
21526
  credentials: CredentialStateSchema,
20536
- refreshCredentials: method2(Type7.Null(), Type7.Null())
21527
+ refreshCredentials: method2(Type8.Null(), Type8.Null())
20537
21528
  }
20538
21529
  });
20539
21530
  });
@@ -20746,12 +21737,13 @@ async function connectToGateway(options = {}) {
20746
21737
  const storagePath = options.storagePath ?? config.storagePath ?? DEFAULT_STORAGE_PATH;
20747
21738
  const nekoDir = dirname11(storagePath);
20748
21739
  const fileServerId = readServerId(join12(nekoDir, SERVER_ID_FILENAME2));
20749
- if (process.platform === "win32") {
21740
+ const platform = options.platform ?? process.platform;
21741
+ if (platform === "win32") {
20750
21742
  if (fileServerId !== undefined) {
20751
21743
  const wsUrl = await readGatewayPortFile(nekoDir, fileServerId);
20752
21744
  if (wsUrl !== undefined) {
20753
21745
  try {
20754
- const client = await connectNekoGateway({ transport: "ws", url: wsUrl, serverId: fileServerId });
21746
+ const client = await connectBeforeDeadline({ transport: "ws", url: wsUrl, serverId: fileServerId }, Date.now() + (options.spawnTimeoutMs ?? CONNECT_TIMEOUT_MS));
20755
21747
  return { client, socketPath: undefined, serverId: fileServerId, spawned: false, close: () => client.close() };
20756
21748
  } catch (error) {
20757
21749
  if (error instanceof ApplicationProtocolError)
@@ -20759,7 +21751,7 @@ async function connectToGateway(options = {}) {
20759
21751
  }
20760
21752
  }
20761
21753
  }
20762
- return spawnAndConnect({ ...options, storagePath, socketPath: undefined, nekoDir, fileServerId });
21754
+ return spawnAndConnect({ ...options, platform, storagePath, socketPath: undefined, nekoDir, fileServerId });
20763
21755
  }
20764
21756
  const socketPath = options.socketPath ?? config.gateway?.socketPath ?? (fileServerId !== undefined ? join12(nekoDir, `${fileServerId}.sock`) : undefined);
20765
21757
  const serverIdFor = (path) => fileServerId ?? serverIdFromSocketPath2(path) ?? readServerId(join12(nekoDir, SERVER_ID_FILENAME2));
@@ -20775,7 +21767,7 @@ async function connectToGateway(options = {}) {
20775
21767
  }
20776
21768
  }
20777
21769
  }
20778
- return spawnAndConnect({ ...options, storagePath, socketPath, nekoDir, fileServerId });
21770
+ return spawnAndConnect({ ...options, platform, storagePath, socketPath, nekoDir, fileServerId });
20779
21771
  }
20780
21772
  async function spawnAndConnect(plan) {
20781
21773
  const command = gatewayCommand(gatewayArgs(plan));
@@ -20794,15 +21786,22 @@ async function spawnAndConnect(plan) {
20794
21786
  }
20795
21787
  async function connectSpawned(plan, child, outputTail, spawnError) {
20796
21788
  const timeoutMs = plan.spawnTimeoutMs ?? 20000;
20797
- if (process.platform === "win32") {
21789
+ if (plan.platform === "win32") {
21790
+ const timeoutAt = Date.now() + timeoutMs;
20798
21791
  const serverId = plan.fileServerId ?? await waitForValue2(() => readServerId(join12(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
20799
- const wsUrl = await waitForValue2(() => readGatewayPortFile(plan.nekoDir, serverId), "gateway ready (port file)", timeoutMs, child, outputTail, spawnError);
20800
- let lastError;
20801
- for (let attempt = 0;attempt < CONNECT_RETRIES; attempt++) {
20802
- if (child.exitCode !== null || spawnError() !== undefined)
21792
+ let lastError = new Error("Gateway port file unavailable");
21793
+ while (Date.now() < timeoutAt) {
21794
+ if (child.exitCode !== null || child.signalCode !== null || spawnError() !== undefined) {
21795
+ lastError = spawnError() ?? new Error(`Gateway process exited (code ${child.exitCode}, signal ${child.signalCode})`);
20803
21796
  break;
21797
+ }
21798
+ const wsUrl = await readGatewayPortFile(plan.nekoDir, serverId);
21799
+ if (wsUrl === undefined) {
21800
+ await sleep(Math.min(SOCKET_POLL_MS, Math.max(0, timeoutAt - Date.now())));
21801
+ continue;
21802
+ }
20804
21803
  try {
20805
- const client = await connectNekoGateway({ transport: "ws", url: wsUrl, serverId });
21804
+ const client = await connectBeforeDeadline({ transport: "ws", url: wsUrl, serverId }, timeoutAt, child);
20806
21805
  return {
20807
21806
  client,
20808
21807
  socketPath: undefined,
@@ -20818,11 +21817,11 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
20818
21817
  if (error instanceof ApplicationProtocolError)
20819
21818
  throw error;
20820
21819
  lastError = error;
20821
- await sleep(CONNECT_RETRY_MS);
20822
21820
  }
21821
+ await sleep(Math.min(CONNECT_RETRY_MS, Math.max(0, timeoutAt - Date.now())));
20823
21822
  }
20824
- throw new Error(`Gateway connection failed: ${lastError instanceof Error ? lastError.message : String(lastError)}
20825
- ${outputTail()}`);
21823
+ const timeout = Date.now() >= timeoutAt ? ` (startup timed out after ${timeoutMs}ms)` : "";
21824
+ throw new Error(`Gateway connection failed${timeout}: ${lastError instanceof Error ? lastError.message : String(lastError)}${formatTail(outputTail())}`);
20826
21825
  }
20827
21826
  let socketPath = plan.socketPath;
20828
21827
  if (socketPath === undefined) {
@@ -20861,7 +21860,7 @@ ${outputTail()}`);
20861
21860
  ${outputTail()}`);
20862
21861
  }
20863
21862
  function gatewayArgs(plan) {
20864
- if (process.platform === "win32") {
21863
+ if (plan.platform === "win32") {
20865
21864
  return [
20866
21865
  "--no-socket",
20867
21866
  "--storage",
@@ -20895,6 +21894,25 @@ function serverIdFromSocketPath2(socketPath) {
20895
21894
  const candidate = name.slice(0, -".sock".length);
20896
21895
  return isServerId(candidate) ? candidate : undefined;
20897
21896
  }
21897
+ async function connectBeforeDeadline(address, timeoutAt, child) {
21898
+ const remaining = timeoutAt - Date.now();
21899
+ if (remaining <= 0)
21900
+ throw new Error("Gateway connection timed out");
21901
+ const controller = new AbortController;
21902
+ const timer = setTimeout(() => controller.abort(new Error("Gateway connection timed out")), Math.min(CONNECT_TIMEOUT_MS, remaining));
21903
+ const exited = () => controller.abort(new Error("Gateway process exited during connection"));
21904
+ child?.once("exit", exited);
21905
+ try {
21906
+ return await connectNekoGateway(address, controller.signal);
21907
+ } catch (error) {
21908
+ if (controller.signal.aborted)
21909
+ throw controller.signal.reason;
21910
+ throw error;
21911
+ } finally {
21912
+ clearTimeout(timer);
21913
+ child?.removeListener("exit", exited);
21914
+ }
21915
+ }
20898
21916
  function captureOutput(child) {
20899
21917
  let stdout = "";
20900
21918
  let stderr = "";
@@ -20916,8 +21934,8 @@ async function waitForValue2(poll, what, timeoutMs, child, outputTail, spawnErro
20916
21934
  const value = await poll();
20917
21935
  if (value !== undefined)
20918
21936
  return value;
20919
- if (child.exitCode !== null) {
20920
- throw new Error(`Gateway process exited early (exit ${child.exitCode})${formatTail(outputTail())}`);
21937
+ if (child.exitCode !== null || child.signalCode !== null) {
21938
+ throw new Error(`Gateway process exited early (exit ${child.exitCode}, signal ${child.signalCode})${formatTail(outputTail())}`);
20921
21939
  }
20922
21940
  if (Date.now() - start > timeoutMs) {
20923
21941
  child.kill("SIGTERM");
@@ -20934,7 +21952,7 @@ ${tail}`;
20934
21952
  function sleep(ms) {
20935
21953
  return new Promise((resolve) => setTimeout(resolve, ms));
20936
21954
  }
20937
- var SERVER_ID_FILENAME2 = "gateway-server-id", SOCKET_POLL_MS = 50, CONNECT_RETRY_MS = 100, CONNECT_RETRIES = 20;
21955
+ var SERVER_ID_FILENAME2 = "gateway-server-id", SOCKET_POLL_MS = 50, CONNECT_RETRY_MS = 100, CONNECT_RETRIES = 20, CONNECT_TIMEOUT_MS = 5000;
20938
21956
  var init_gateway2 = __esm(() => {
20939
21957
  init_src();
20940
21958
  init_application_version();
@@ -20942,7 +21960,7 @@ var init_gateway2 = __esm(() => {
20942
21960
  });
20943
21961
 
20944
21962
  // packages/tui/src/help-ui.ts
20945
- import { getKeybindings, matchesKey as matchesKey6, TUI_KEYBINDINGS, wrapTextWithAnsi as wrapTextWithAnsi3 } from "@earendil-works/pi-tui";
21963
+ import { getKeybindings, matchesKey as matchesKey7, TUI_KEYBINDINGS, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
20946
21964
  function helpLines(platform = process.platform) {
20947
21965
  const rows = [];
20948
21966
  const section = (title, hints) => {
@@ -20980,6 +21998,13 @@ function helpLines(platform = process.platform) {
20980
21998
  ["S", "Stop current round; keep agent context (Enter confirms)"],
20981
21999
  ["Click", "Open round card or status-bar Agents entry"]
20982
22000
  ]);
22001
+ section("Jobs", [
22002
+ ["/jobs", "View background jobs"],
22003
+ ["Enter", "Open job output"],
22004
+ ["S", "Stop selected running job (Enter confirms)"],
22005
+ ["End", "Jump to the newest output"],
22006
+ ["\u2190", "Back"]
22007
+ ]);
20983
22008
  section("Skills", [
20984
22009
  ["U", "Install for user"],
20985
22010
  ["P", "Install for project"],
@@ -21032,20 +22057,20 @@ class HelpDetail {
21032
22057
  this.#offset = Math.max(0, Math.min(this.#max, this.#offset + delta));
21033
22058
  }
21034
22059
  handleKey(data) {
21035
- if (matchesKey6(data, "up"))
22060
+ if (matchesKey7(data, "up"))
21036
22061
  this.scroll(-1);
21037
- else if (matchesKey6(data, "down"))
22062
+ else if (matchesKey7(data, "down"))
21038
22063
  this.scroll(1);
21039
- else if (matchesKey6(data, "pageUp"))
22064
+ else if (matchesKey7(data, "pageUp"))
21040
22065
  this.scroll(-this.#page);
21041
- else if (matchesKey6(data, "pageDown"))
22066
+ else if (matchesKey7(data, "pageDown"))
21042
22067
  this.scroll(this.#page);
21043
22068
  else
21044
22069
  return false;
21045
22070
  return true;
21046
22071
  }
21047
22072
  lines(width, height) {
21048
- const rows = helpLines().flatMap((line) => wrapTextWithAnsi3(line, width));
22073
+ const rows = helpLines().flatMap((line) => wrapTextWithAnsi4(line, width));
21049
22074
  this.#max = Math.max(0, rows.length - height);
21050
22075
  this.#page = Math.max(1, height - 1);
21051
22076
  this.#offset = Math.min(this.#offset, this.#max);
@@ -21065,7 +22090,7 @@ import { readFile as readFile8, stat as stat5 } from "fs/promises";
21065
22090
  import { homedir as homedir8 } from "os";
21066
22091
  import { basename as basename8, resolve as resolve5 } from "path";
21067
22092
  import { promisify as promisify2 } from "util";
21068
- import { isKeyRelease as isKeyRelease4, isKeyRepeat as isKeyRepeat2, matchesKey as matchesKey7 } from "@earendil-works/pi-tui";
22093
+ import { isKeyRelease as isKeyRelease4, isKeyRepeat as isKeyRepeat2, matchesKey as matchesKey8 } from "@earendil-works/pi-tui";
21069
22094
  function imagePaths(text, platform = process.platform) {
21070
22095
  const paths = [];
21071
22096
  let word = "", quote = "", escaped = false;
@@ -21159,7 +22184,7 @@ end try`]).catch(() => ""));
21159
22184
  return { text: String(await run(tool, wayland ? ["--no-newline"] : ["-selection", "clipboard", "-o"])) };
21160
22185
  }
21161
22186
  function isClipboardShortcut(data, platform = process.platform) {
21162
- return !isKeyRelease4(data) && !isKeyRepeat2(data) && matchesKey7(data, platform === "win32" ? "alt+v" : "ctrl+v");
22187
+ return !isKeyRelease4(data) && !isKeyRepeat2(data) && matchesKey8(data, platform === "win32" ? "alt+v" : "ctrl+v");
21163
22188
  }
21164
22189
  var exec;
21165
22190
  var init_image_input = __esm(() => {
@@ -21167,7 +22192,7 @@ var init_image_input = __esm(() => {
21167
22192
  });
21168
22193
 
21169
22194
  // packages/tui/src/layout.ts
21170
- import { Text as Text7, VStack } from "@earendil-works/pi-tui";
22195
+ import { Text as Text8, VStack } from "@earendil-works/pi-tui";
21171
22196
  function createAppLayout(parts) {
21172
22197
  const normal = (v) => {
21173
22198
  if (parts.approval)
@@ -21179,7 +22204,7 @@ function createAppLayout(parts) {
21179
22204
  };
21180
22205
  return new VStack([
21181
22206
  {
21182
- component: new Text7("Terminal too small. Resize to 40x10. Ctrl+C Exit", 0, 0),
22207
+ component: new Text8("Terminal too small. Resize to 40x10. Ctrl+C Exit", 0, 0),
21183
22208
  visible: (v) => !normal(v),
21184
22209
  grow: 1
21185
22210
  },
@@ -21187,13 +22212,13 @@ function createAppLayout(parts) {
21187
22212
  { component: parts.panel, basis: 0, grow: 1, minSize: 1, visible: (v) => normal(v) && parts.panelOpen() },
21188
22213
  { component: parts.notices, maxSize: 1, shrink: 0, visible: normal },
21189
22214
  {
21190
- component: parts.busy ?? new Text7("", 0, 0),
22215
+ component: parts.busy ?? new Text8("", 0, 0),
21191
22216
  maxSize: 2,
21192
22217
  shrink: 0,
21193
22218
  visible: (v) => normal(v) && (parts.busyVisible?.() ?? false)
21194
22219
  },
21195
22220
  {
21196
- component: parts.approval ?? new Text7("", 0, 0),
22221
+ component: parts.approval ?? new Text8("", 0, 0),
21197
22222
  shrink: 0,
21198
22223
  visible: (v) => normal(v) && (parts.approvalVisible?.() ?? false)
21199
22224
  },
@@ -21206,7 +22231,7 @@ var init_layout = () => {};
21206
22231
 
21207
22232
  // packages/tui/src/skill-market-ui.ts
21208
22233
  import { ChordError as ChordError4 } from "@earendil-works/chord";
21209
- import { Markdown as Markdown3, matchesKey as matchesKey8, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
22234
+ import { Markdown as Markdown3, matchesKey as matchesKey9, wrapTextWithAnsi as wrapTextWithAnsi5 } from "@earendil-works/pi-tui";
21210
22235
 
21211
22236
  class SkillDetailView {
21212
22237
  #skill = null;
@@ -21245,11 +22270,11 @@ class SkillDetailView {
21245
22270
  }
21246
22271
  handleKey(data) {
21247
22272
  if (this.#confirm) {
21248
- if (matchesKey8(data, "y")) {
22273
+ if (matchesKey9(data, "y")) {
21249
22274
  const pending = this.#confirm;
21250
22275
  this.#confirm = null;
21251
22276
  this.run(pending.run);
21252
- } else if (matchesKey8(data, "n")) {
22277
+ } else if (matchesKey9(data, "n")) {
21253
22278
  this.#confirm = null;
21254
22279
  }
21255
22280
  return true;
@@ -21258,17 +22283,17 @@ class SkillDetailView {
21258
22283
  return true;
21259
22284
  const skill = this.#skill;
21260
22285
  const builtinOnly = skill.installed.builtin && !skill.installed.user && !skill.installed.project;
21261
- if (matchesKey8(data, "v")) {
22286
+ if (matchesKey9(data, "v")) {
21262
22287
  this.#openPreview(this.#name);
21263
22288
  return true;
21264
22289
  }
21265
22290
  if (builtinOnly)
21266
22291
  return true;
21267
- if (matchesKey8(data, "u"))
22292
+ if (matchesKey9(data, "u"))
21268
22293
  this.askInstall("user", skill);
21269
- else if (matchesKey8(data, "p"))
22294
+ else if (matchesKey9(data, "p"))
21270
22295
  this.askInstall("project", skill);
21271
- else if (matchesKey8(data, "d"))
22296
+ else if (matchesKey9(data, "d"))
21272
22297
  this.askUninstall(skill);
21273
22298
  else
21274
22299
  return false;
@@ -21340,7 +22365,7 @@ class SkillDetailView {
21340
22365
  }
21341
22366
  lines(width, _height) {
21342
22367
  if (this.#confirm) {
21343
- return [theme.accent("Confirm"), "", ...wrapTextWithAnsi4(this.#confirm.prompt, width), ""];
22368
+ return [theme.accent("Confirm"), "", ...wrapTextWithAnsi5(this.#confirm.prompt, width), ""];
21344
22369
  }
21345
22370
  const skill = this.#skill;
21346
22371
  if (skill === null)
@@ -21356,7 +22381,7 @@ class SkillDetailView {
21356
22381
  theme.dim(`${skill.source} \xB7 ${skill.category} \xB7 ${formatSize(skill.sizeBytes)} \xB7 sha256 ${skill.sha256.slice(0, 10)}\u2026`),
21357
22382
  `${theme.dim("installed:")} ${state}${skill.activeLevel ? theme.dim(` (active: ${skill.activeLevel})`) : ""}`,
21358
22383
  "",
21359
- ...wrapTextWithAnsi4(skill.description, width)
22384
+ ...wrapTextWithAnsi5(skill.description, width)
21360
22385
  ];
21361
22386
  if (this.#busy)
21362
22387
  rows.push("", theme.dim("Working..."));
@@ -21393,13 +22418,13 @@ class SkillPreviewView {
21393
22418
  this.#offset = Math.max(0, Math.min(this.#maxOffset, this.#offset + delta));
21394
22419
  }
21395
22420
  handleKey(data) {
21396
- if (matchesKey8(data, "up"))
22421
+ if (matchesKey9(data, "up"))
21397
22422
  this.scroll(-1);
21398
- else if (matchesKey8(data, "down"))
22423
+ else if (matchesKey9(data, "down"))
21399
22424
  this.scroll(1);
21400
- else if (matchesKey8(data, "pageUp"))
22425
+ else if (matchesKey9(data, "pageUp"))
21401
22426
  this.scroll(-this.#pageSize);
21402
- else if (matchesKey8(data, "pageDown"))
22427
+ else if (matchesKey9(data, "pageDown"))
21403
22428
  this.scroll(this.#pageSize);
21404
22429
  else
21405
22430
  return false;
@@ -21451,7 +22476,7 @@ var init_skill_market_ui = __esm(() => {
21451
22476
  });
21452
22477
 
21453
22478
  // packages/tui/src/subagents-ui.ts
21454
- import { matchesKey as matchesKey9 } from "@earendil-works/pi-tui";
22479
+ import { matchesKey as matchesKey10 } from "@earendil-works/pi-tui";
21455
22480
 
21456
22481
  class SubagentDetail {
21457
22482
  get help() {
@@ -21571,13 +22596,13 @@ class SubagentDetail {
21571
22596
  this.history();
21572
22597
  }
21573
22598
  handleKey(data) {
21574
- if (matchesKey9(data, "up")) {
22599
+ if (matchesKey10(data, "up")) {
21575
22600
  this.scroll(-1);
21576
- } else if (matchesKey9(data, "down")) {
22601
+ } else if (matchesKey10(data, "down")) {
21577
22602
  this.scroll(1);
21578
- } else if (matchesKey9(data, "pageUp")) {
22603
+ } else if (matchesKey10(data, "pageUp")) {
21579
22604
  this.scroll(-this.#pageSize);
21580
- } else if (matchesKey9(data, "pageDown")) {
22605
+ } else if (matchesKey10(data, "pageDown")) {
21581
22606
  this.scroll(this.#pageSize);
21582
22607
  } else if (data.toLowerCase() === "e") {
21583
22608
  this.#expanded = !this.#expanded;
@@ -21593,7 +22618,7 @@ class SubagentDetail {
21593
22618
  help: shortcutHints(["Enter", "Stop"], ["Esc", "Back"]),
21594
22619
  lines: () => [theme.warning("Stop this task? Existing changes will be kept.")],
21595
22620
  handleKey: (key) => {
21596
- if (!matchesKey9(key, "enter"))
22621
+ if (!matchesKey10(key, "enter"))
21597
22622
  return false;
21598
22623
  this.#service.stop(stopTaskId === undefined ? null : { taskId: stopTaskId }).catch((e) => {
21599
22624
  this.#error = String(e);
@@ -21724,7 +22749,7 @@ var init_subagents_ui = __esm(() => {
21724
22749
  });
21725
22750
 
21726
22751
  // packages/tui/src/version-ui.ts
21727
- import { matchesKey as matchesKey10, Text as Text8 } from "@earendil-works/pi-tui";
22752
+ import { matchesKey as matchesKey11, Text as Text9 } from "@earendil-works/pi-tui";
21728
22753
  function actionFor(state, installed, runningVersion) {
21729
22754
  const action = updateAction({ gateway: state, frontend: runningVersion, installed });
21730
22755
  return action === "install" && !newer(state?.latestVersion, runningVersion) ? "none" : action;
@@ -21797,14 +22822,14 @@ class VersionDetail {
21797
22822
  this.body = body;
21798
22823
  }
21799
22824
  lines(width, height) {
21800
- const lines = new Text8(this.body, 0, 0).render(width);
22825
+ const lines = new Text9(this.body, 0, 0).render(width);
21801
22826
  this.#offset = Math.min(this.#offset, Math.max(0, lines.length - height));
21802
22827
  return lines.slice(this.#offset, this.#offset + height);
21803
22828
  }
21804
22829
  handleKey(data) {
21805
- if (matchesKey10(data, "up"))
22830
+ if (matchesKey11(data, "up"))
21806
22831
  this.#offset = Math.max(0, this.#offset - 1);
21807
- else if (matchesKey10(data, "down"))
22832
+ else if (matchesKey11(data, "down"))
21808
22833
  this.#offset++;
21809
22834
  else
21810
22835
  return false;
@@ -21866,10 +22891,10 @@ var init_workspace2 = () => {};
21866
22891
 
21867
22892
  // packages/tui/src/tui.ts
21868
22893
  import {
21869
- Container as Container5,
22894
+ Container as Container6,
21870
22895
  isKeyRelease as isKeyRelease5,
21871
22896
  Loader,
21872
- matchesKey as matchesKey11,
22897
+ matchesKey as matchesKey12,
21873
22898
  ProcessTerminal,
21874
22899
  ScrollView,
21875
22900
  TuiAltScreen
@@ -21881,8 +22906,8 @@ class NekoTuiApp {
21881
22906
  #options;
21882
22907
  #ui;
21883
22908
  #imageTranscript = new ImageTranscript(() => this.#transcript);
21884
- #notices = new Container5;
21885
- #versionNotice = new Container5;
22909
+ #notices = new Container6;
22910
+ #versionNotice = new Container6;
21886
22911
  #versionStop;
21887
22912
  #versionTimer;
21888
22913
  #terminal = new ProcessTerminal;
@@ -21904,6 +22929,7 @@ class NekoTuiApp {
21904
22929
  #toolSpinner = new ToolSpinnerTicker(() => this.#onToolSpinnerTick());
21905
22930
  #chat;
21906
22931
  #agentsPanel = false;
22932
+ #jobsPanel = false;
21907
22933
  #agentDetails = new Map;
21908
22934
  #sessionId = "";
21909
22935
  #viewUnsubscribe;
@@ -21944,6 +22970,7 @@ class NekoTuiApp {
21944
22970
  this.#panel = new PickerPanel(() => this.#ui.requestRender(), () => {
21945
22971
  this.#thinkingModel = undefined;
21946
22972
  this.#agentsPanel = false;
22973
+ this.#jobsPanel = false;
21947
22974
  clearInterval(this.#sessionPanelTimer);
21948
22975
  this.#sessionPanelTimer = undefined;
21949
22976
  this.#ui.setFocus(this.#editor);
@@ -22004,10 +23031,10 @@ class NekoTuiApp {
22004
23031
  this.#ui.addInputListener((data) => {
22005
23032
  if (isKeyRelease5(data))
22006
23033
  return { consume: true };
22007
- if (!matchesKey11(data, "escape") || this.#panel.open || this.#login.open || this.#busyText !== undefined || this.#lastView?.compaction?.active.length)
23034
+ if (!matchesKey12(data, "escape") || this.#panel.open || this.#login.open || this.#busyText !== undefined || this.#lastView?.compaction?.active.length)
22008
23035
  this.#editor.resetEscapeSequence();
22009
23036
  if (!usableSize(this.#terminal.columns, this.#terminal.rows)) {
22010
- if (matchesKey11(data, "ctrl+c"))
23037
+ if (matchesKey12(data, "ctrl+c"))
22011
23038
  this.exit();
22012
23039
  return { consume: true };
22013
23040
  }
@@ -22018,11 +23045,11 @@ class NekoTuiApp {
22018
23045
  }
22019
23046
  if (this.#sessionOperation)
22020
23047
  return { consume: true };
22021
- if (matchesKey11(data, "right") && this.#lastView?.subagents?.length && this.#editor.getText() === "" && !this.#editor.isShowingAutocomplete()) {
23048
+ if (matchesKey12(data, "right") && this.#lastView?.subagents?.length && this.#editor.getText() === "" && !this.#editor.isShowingAutocomplete()) {
22022
23049
  this.#showAgents();
22023
23050
  return { consume: true };
22024
23051
  }
22025
- if (matchesKey11(data, process.platform === "win32" ? "alt+v" : "ctrl+v")) {
23052
+ if (matchesKey12(data, process.platform === "win32" ? "alt+v" : "ctrl+v")) {
22026
23053
  if (isClipboardShortcut(data))
22027
23054
  this.pasteClipboard();
22028
23055
  return { consume: true };
@@ -22039,26 +23066,26 @@ class NekoTuiApp {
22039
23066
  return { consume: true };
22040
23067
  }
22041
23068
  }
22042
- if (matchesKey11(data, "escape") && this.#editor.isShowingAutocomplete()) {
23069
+ if (matchesKey12(data, "escape") && this.#editor.isShowingAutocomplete()) {
22043
23070
  this.#editor.resetEscapeSequence();
22044
23071
  return;
22045
23072
  }
22046
- if (matchesKey11(data, "ctrl+c")) {
23073
+ if (matchesKey12(data, "ctrl+c")) {
22047
23074
  if (this.#busyText !== undefined)
22048
23075
  this.abort();
22049
23076
  else
22050
23077
  this.exit();
22051
23078
  return { consume: true };
22052
23079
  }
22053
- if (matchesKey11(data, "escape") && (this.#busyText !== undefined || this.#lastView?.compaction?.active.some((c) => c.reason === "manual" || c.blocking))) {
23080
+ if (matchesKey12(data, "escape") && (this.#busyText !== undefined || this.#lastView?.compaction?.active.some((c) => c.reason === "manual" || c.blocking))) {
22054
23081
  this.abort();
22055
23082
  return { consume: true };
22056
23083
  }
22057
- if (matchesKey11(data, "ctrl+s")) {
23084
+ if (matchesKey12(data, "ctrl+s")) {
22058
23085
  this.openSessionsOverlay();
22059
23086
  return { consume: true };
22060
23087
  }
22061
- if (this.#busyText !== undefined && matchesKey11(data, "alt+enter")) {
23088
+ if (this.#busyText !== undefined && matchesKey12(data, "alt+enter")) {
22062
23089
  this.submitFollowUp();
22063
23090
  return { consume: true };
22064
23091
  }
@@ -22075,10 +23102,10 @@ class NekoTuiApp {
22075
23102
  this.addNotice("Gateway unavailable. Restart to reconnect.", "error");
22076
23103
  });
22077
23104
  const picker = this.#panel, login = this.#login;
22078
- const content = new Container5;
23105
+ const content = new Container6;
22079
23106
  content.addChild(this.#imageTranscript);
22080
23107
  this.#transcript = new ScrollView(content, { follow: "end", primary: true, overscroll: "chain" });
22081
- const notices = new Container5;
23108
+ const notices = new Container6;
22082
23109
  notices.addChild(this.#notices);
22083
23110
  notices.addChild(this.#versionNotice);
22084
23111
  this.#ui.setLayoutRoot(createAppLayout({
@@ -22161,6 +23188,10 @@ class NekoTuiApp {
22161
23188
  this.#showAgents();
22162
23189
  return;
22163
23190
  }
23191
+ if (trimmed === "/jobs") {
23192
+ this.#showJobs();
23193
+ return;
23194
+ }
22164
23195
  const instructions = compactInstructions(trimmed);
22165
23196
  if (instructions !== null) {
22166
23197
  const chat = this.#chat;
@@ -22399,6 +23430,8 @@ class NekoTuiApp {
22399
23430
  async switchSession(id) {
22400
23431
  this.#approval.close();
22401
23432
  this.#chat = undefined;
23433
+ if (this.#jobsPanel && this.#panel.open)
23434
+ this.#panel.close();
22402
23435
  if (this.#sessionId)
22403
23436
  this.#texts.set(this.#sessionId, this.#editor.getText());
22404
23437
  this.#viewUnsubscribe?.();
@@ -22586,6 +23619,36 @@ class NekoTuiApp {
22586
23619
  if (taskId !== undefined)
22587
23620
  this.#panel.showDetail(this.#agentDetail(taskId));
22588
23621
  }
23622
+ #showJobs() {
23623
+ if (this.#panel.open || this.#login.open)
23624
+ return;
23625
+ const chat = this.#chat;
23626
+ if (chat === undefined)
23627
+ return;
23628
+ const jobs = jobsClient(chat);
23629
+ if (!jobs.supported) {
23630
+ this.addNotice(JOBS_UNSUPPORTED, "warning");
23631
+ return;
23632
+ }
23633
+ const changed = () => this.#ui.requestRender();
23634
+ this.#jobsPanel = true;
23635
+ this.#panel.pointerSelection = true;
23636
+ this.#panel.emptyText = JOBS_EMPTY;
23637
+ this.#panel.listKey = (data, value) => {
23638
+ if (matchesKey12(data, "left")) {
23639
+ this.#panel.close();
23640
+ return true;
23641
+ }
23642
+ if (data.toLowerCase() !== "s" || this.#panel.search.getValue() !== "" || value === undefined)
23643
+ return false;
23644
+ if (jobs.list().find((job) => job.jobId === value)?.status !== "running")
23645
+ return false;
23646
+ confirmStop(this.#panel, jobs, value, (message) => this.addNotice(message, "error"), changed);
23647
+ return true;
23648
+ };
23649
+ this.#panel.show("Jobs", async () => jobItems(jobs.list()), async (value) => new JobDetail(jobs, value, this.#panel, changed).open());
23650
+ this.#ui.setFocus(this.#panel);
23651
+ }
22589
23652
  #agentDetail(target) {
22590
23653
  const round = typeof target === "number" ? this.#lastView?.subagents?.find((r) => r.taskId === target) : undefined;
22591
23654
  const id = typeof target === "string" ? target : round?.agentId ?? `agent-${round?.childId}`;
@@ -22650,6 +23713,8 @@ class NekoTuiApp {
22650
23713
  this.#approval.close();
22651
23714
  if (this.#agentsPanel)
22652
23715
  this.#panel.update(subagentItems(view.subagents ?? [], view.agents));
23716
+ if (this.#jobsPanel)
23717
+ this.#panel.update(jobItems(view.jobs ?? []));
22653
23718
  for (const identity of view.agents ?? []) {
22654
23719
  const detail = this.#agentDetails.get(identity.agentId);
22655
23720
  if (detail) {
@@ -22783,6 +23848,12 @@ class NekoTuiApp {
22783
23848
  this.#items.set(item.key, { kind: "tool", component });
22784
23849
  return;
22785
23850
  }
23851
+ if (item.jobId !== undefined) {
23852
+ const component = new JobReportNotice(item.text);
23853
+ this.#imageTranscript.addItem(component);
23854
+ this.#items.set(item.key, { kind: "job-report", component });
23855
+ return;
23856
+ }
22786
23857
  const component = new ThemedText(item.text, theme.dim, 1, 0);
22787
23858
  this.#imageTranscript.addItem(component);
22788
23859
  this.#items.set(item.key, { kind: "system", component });
@@ -22800,7 +23871,7 @@ class NekoTuiApp {
22800
23871
  entry.component.sync(mergeStream(item, streamCall));
22801
23872
  return;
22802
23873
  }
22803
- if (entry.kind === "system" && item.type === "system") {
23874
+ if ((entry.kind === "system" || entry.kind === "job-report") && item.type === "system") {
22804
23875
  entry.component.setText(item.text);
22805
23876
  }
22806
23877
  }
@@ -22860,6 +23931,7 @@ class NekoTuiApp {
22860
23931
  const usage = view.taskUsage ?? view.usage;
22861
23932
  this.#footer.setStats({
22862
23933
  subagentCount: view.agents?.length ?? 0,
23934
+ jobCount: view.jobs?.filter((job) => job.status === "running").length ?? 0,
22863
23935
  approvalCount: view.subagents?.reduce((n, d) => n + (d.approvalCount ?? 0), 0) ?? 0,
22864
23936
  model: view.model,
22865
23937
  thinkingLevel: view.agent.thinkingLevel,
@@ -22981,12 +24053,14 @@ var init_tui = __esm(() => {
22981
24053
  init_src();
22982
24054
  init_application_updates();
22983
24055
  init_input_history();
24056
+ init_client_session();
22984
24057
  init_approval_dock();
22985
24058
  init_clipboard_write();
22986
24059
  init_assistant_message();
22987
24060
  init_editor();
22988
24061
  init_footer();
22989
24062
  init_history_image();
24063
+ init_job_report();
22990
24064
  init_login();
22991
24065
  init_notice();
22992
24066
  init_picker();
@@ -22999,6 +24073,7 @@ var init_tui = __esm(() => {
22999
24073
  init_gateway2();
23000
24074
  init_help_ui();
23001
24075
  init_image_input();
24076
+ init_jobs_ui();
23002
24077
  init_labels();
23003
24078
  init_layout();
23004
24079
  init_skill_market_ui();