neko-harness 0.4.2 → 0.4.4

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
@@ -80,7 +80,7 @@ function updateAction(input) {
80
80
  return "install";
81
81
  return "none";
82
82
  }
83
- var RUNNING_VERSION = "0.4.2", APPLICATION_PROTOCOL = 4, ApplicationProtocolError;
83
+ var RUNNING_VERSION = "0.4.4", APPLICATION_PROTOCOL = 6, ApplicationProtocolError;
84
84
  var init_application_version = __esm(() => {
85
85
  ApplicationProtocolError = class ApplicationProtocolError extends Error {
86
86
  constructor() {
@@ -375,6 +375,182 @@ var init_update = __esm(() => {
375
375
  init_application_version();
376
376
  });
377
377
 
378
+ // packages/core/src/context-limits.ts
379
+ var PERCENT, CONTEXT_FIELD_LIMITS, PRUNING_DEFAULTS;
380
+ var init_context_limits = __esm(() => {
381
+ PERCENT = { kind: "int", min: 1, max: 99 };
382
+ CONTEXT_FIELD_LIMITS = {
383
+ compaction: {
384
+ enabled: { kind: "boolean" },
385
+ triggerPercent: PERCENT,
386
+ keepRecentTokens: { kind: "int", min: 1, max: 1e8 }
387
+ },
388
+ pruning: {
389
+ enabled: { kind: "boolean" },
390
+ triggerPercent: PERCENT,
391
+ keepRecent: { kind: "int", min: 0, max: 1e5 },
392
+ minChars: { kind: "int", min: 1, max: 1e8 },
393
+ minSavePercent: PERCENT
394
+ }
395
+ };
396
+ PRUNING_DEFAULTS = {
397
+ enabled: false,
398
+ triggerPercent: 35,
399
+ keepRecent: 10,
400
+ minChars: 4000,
401
+ minSavePercent: 5
402
+ };
403
+ });
404
+
405
+ // packages/core/src/context-settings.ts
406
+ import { DEFAULT_COMPACTION_POLICY } from "@earendil-works/pi-durable";
407
+ function describe(field) {
408
+ return field.kind === "boolean" ? "a boolean" : `an integer between ${field.min} and ${field.max}`;
409
+ }
410
+ function validateGroups(value, prefix, allowNull) {
411
+ if (!isObject(value))
412
+ throw new Error(`"${prefix}" must be an object.`);
413
+ const out = {};
414
+ for (const key of Object.keys(value)) {
415
+ if (!(key in SCHEMA))
416
+ throw new Error(`"${prefix}.${key}" is not a known setting.`);
417
+ }
418
+ for (const [group, fields] of Object.entries(SCHEMA)) {
419
+ const given = value[group];
420
+ if (given === undefined)
421
+ continue;
422
+ if (!isObject(given))
423
+ throw new Error(`"${prefix}.${group}" must be an object.`);
424
+ const target = {};
425
+ for (const key of Object.keys(given)) {
426
+ if (!(key in fields))
427
+ throw new Error(`"${prefix}.${group}.${key}" is not a known setting.`);
428
+ }
429
+ for (const [name, field] of Object.entries(fields)) {
430
+ const item = given[name];
431
+ if (item === undefined)
432
+ continue;
433
+ if (item === null && allowNull) {
434
+ target[name] = null;
435
+ continue;
436
+ }
437
+ const ok = field.kind === "boolean" ? typeof item === "boolean" : typeof item === "number" && Number.isInteger(item) && item >= field.min && item <= field.max;
438
+ if (!ok)
439
+ throw new Error(`"${prefix}.${group}.${name}" must be ${describe(field)}.`);
440
+ target[name] = item;
441
+ }
442
+ out[group] = target;
443
+ }
444
+ return out;
445
+ }
446
+ function parseContextSettings(value, prefix = "context") {
447
+ return validateGroups(value, prefix, false);
448
+ }
449
+ function parseContextSettingsPatch(value) {
450
+ return validateGroups(value, "context", true);
451
+ }
452
+ function applyContextPatch(saved, patch) {
453
+ const next = {
454
+ compaction: { ...saved.compaction },
455
+ pruning: { ...saved.pruning }
456
+ };
457
+ for (const [key, value] of Object.entries(patch.compaction ?? {})) {
458
+ if (value !== undefined)
459
+ next.compaction[key] = value;
460
+ }
461
+ for (const [key, value] of Object.entries(patch.pruning ?? {})) {
462
+ if (value !== undefined)
463
+ next.pruning[key] = value;
464
+ }
465
+ return next;
466
+ }
467
+ function sanitizeSavedContextSettings(raw) {
468
+ const saved = emptySavedContextSettings();
469
+ if (!isObject(raw))
470
+ return saved;
471
+ for (const [group, fields] of Object.entries(SCHEMA)) {
472
+ const stored = raw[group];
473
+ if (!isObject(stored))
474
+ continue;
475
+ for (const [name, field] of Object.entries(fields)) {
476
+ const item = stored[name];
477
+ const ok = field.kind === "boolean" ? typeof item === "boolean" : typeof item === "number" && Number.isInteger(item) && item >= field.min && item <= field.max;
478
+ if (ok)
479
+ saved[group][name] = item;
480
+ }
481
+ }
482
+ return saved;
483
+ }
484
+ function pick(saved, config, fallback) {
485
+ if (saved !== null && saved !== undefined)
486
+ return { value: saved, source: "saved" };
487
+ if (config !== undefined)
488
+ return { value: config, source: "config" };
489
+ return { value: fallback, source: "default" };
490
+ }
491
+ function resolveContextSettings(config, saved) {
492
+ const cEnabled = pick(saved.compaction.enabled, config?.compaction?.enabled, DEFAULT_COMPACTION_POLICY.enabled);
493
+ const cTrigger = pick(saved.compaction.triggerPercent, config?.compaction?.triggerPercent, null);
494
+ const cKeep = pick(saved.compaction.keepRecentTokens, config?.compaction?.keepRecentTokens, DEFAULT_COMPACTION_POLICY.keepRecentTokens);
495
+ const pEnabled = pick(saved.pruning.enabled, config?.pruning?.enabled, PRUNING_DEFAULTS.enabled);
496
+ const pTrigger = pick(saved.pruning.triggerPercent, config?.pruning?.triggerPercent, PRUNING_DEFAULTS.triggerPercent);
497
+ const pKeep = pick(saved.pruning.keepRecent, config?.pruning?.keepRecent, PRUNING_DEFAULTS.keepRecent);
498
+ const pMin = pick(saved.pruning.minChars, config?.pruning?.minChars, PRUNING_DEFAULTS.minChars);
499
+ const pSave = pick(saved.pruning.minSavePercent, config?.pruning?.minSavePercent, PRUNING_DEFAULTS.minSavePercent);
500
+ return {
501
+ effective: {
502
+ compaction: { enabled: cEnabled.value, triggerPercent: cTrigger.value, keepRecentTokens: cKeep.value },
503
+ pruning: {
504
+ enabled: pEnabled.value,
505
+ triggerPercent: pTrigger.value,
506
+ keepRecent: pKeep.value,
507
+ minChars: pMin.value,
508
+ minSavePercent: pSave.value
509
+ }
510
+ },
511
+ sources: {
512
+ compaction: { enabled: cEnabled.source, triggerPercent: cTrigger.source, keepRecentTokens: cKeep.source },
513
+ pruning: {
514
+ enabled: pEnabled.source,
515
+ triggerPercent: pTrigger.source,
516
+ keepRecent: pKeep.source,
517
+ minChars: pMin.source,
518
+ minSavePercent: pSave.source
519
+ }
520
+ }
521
+ };
522
+ }
523
+ function compactionPolicy(state, windows) {
524
+ const { effective, sources } = state;
525
+ const policy = {};
526
+ if (sources.compaction.enabled !== "default")
527
+ policy.enabled = effective.compaction.enabled;
528
+ if (sources.compaction.keepRecentTokens !== "default")
529
+ policy.keepRecentTokens = effective.compaction.keepRecentTokens;
530
+ const percent = effective.compaction.triggerPercent;
531
+ const window2 = windows.length > 0 ? Math.min(...windows) : 0;
532
+ if (percent !== null && window2 > 0)
533
+ policy.reserveTokens = window2 - Math.floor(window2 * percent / 100);
534
+ return policy;
535
+ }
536
+ var emptySavedContextSettings = () => ({
537
+ compaction: { enabled: null, triggerPercent: null, keepRecentTokens: null },
538
+ pruning: { enabled: null, triggerPercent: null, keepRecent: null, minChars: null, minSavePercent: null }
539
+ }), SCHEMA, isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
540
+ var init_context_settings = __esm(() => {
541
+ init_context_limits();
542
+ SCHEMA = CONTEXT_FIELD_LIMITS;
543
+ });
544
+
545
+ // packages/core/src/search-providers.ts
546
+ function isSearchProvider(value) {
547
+ return typeof value === "string" && SEARCH_PROVIDERS.includes(value);
548
+ }
549
+ var SEARCH_PROVIDERS, DEFAULT_SEARCH_PROVIDER = "tavily";
550
+ var init_search_providers = __esm(() => {
551
+ SEARCH_PROVIDERS = ["tavily", "deepseek"];
552
+ });
553
+
378
554
  // packages/core/src/config.ts
379
555
  import { existsSync, readFileSync } from "fs";
380
556
  import { homedir as homedir2 } from "os";
@@ -489,9 +665,28 @@ function validate(config, source) {
489
665
  throw new Error(`neko config: ${source}: "skills.marketUrl" must be a string.`);
490
666
  }
491
667
  }
668
+ const webSearch = config.webSearch;
669
+ if (webSearch !== undefined) {
670
+ if (typeof webSearch !== "object" || webSearch === null || Array.isArray(webSearch)) {
671
+ throw new Error(`neko config: ${source}: "webSearch" must be an object.`);
672
+ }
673
+ const { provider } = webSearch;
674
+ if (provider !== undefined && !isSearchProvider(provider)) {
675
+ throw new Error(`neko config: ${source}: "webSearch.provider" must be one of ${SEARCH_PROVIDERS.map((item) => `"${item}"`).join(", ")}.`);
676
+ }
677
+ }
678
+ if (config.context !== undefined) {
679
+ try {
680
+ parseContextSettings(config.context);
681
+ } catch (error) {
682
+ throw new Error(`neko config: ${source}: ${error instanceof Error ? error.message : String(error)}`);
683
+ }
684
+ }
492
685
  }
493
686
  var DEFAULT_SKILL_MARKET_URL = "https://skill.kirineko.tech", NEKO_CONFIG_BASENAME = "neko.config.json", ENV_REFERENCE;
494
687
  var init_config = __esm(() => {
688
+ init_context_settings();
689
+ init_search_providers();
495
690
  ENV_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
496
691
  });
497
692
 
@@ -865,6 +1060,52 @@ var init_input = __esm(() => {
865
1060
  NekoInput = defineEntry("app.neko.input");
866
1061
  });
867
1062
 
1063
+ // packages/core/src/queued.ts
1064
+ function queuedUserInput(raw) {
1065
+ const item = raw;
1066
+ if (item.mode !== "steer" && item.mode !== "followUp")
1067
+ return;
1068
+ if (item.entry?.kind !== NekoInput.kind)
1069
+ return;
1070
+ const data = item.entry.data;
1071
+ if (!data || data.jobReport || data.subagentRoundId)
1072
+ return;
1073
+ const defaultPrompt = Array.isArray(item.content) ? savedDefaultImagePrompt(item.content, data.defaultImagePrompt) : undefined;
1074
+ const content = defaultPrompt && Array.isArray(item.content) ? item.content.slice(1) : item.content;
1075
+ const skill = data.skill ? { name: data.skill.name, args: data.skill.args } : undefined;
1076
+ return {
1077
+ id: item.id,
1078
+ mode: item.mode,
1079
+ text: skill ? `/skill:${skill.name}${skill.args ? ` ${skill.args}` : ""}` : textOf(content),
1080
+ images: data.images?.length ?? 0,
1081
+ ...skill ? { skill } : {}
1082
+ };
1083
+ }
1084
+ function projectQueued(view) {
1085
+ const items = view.docs["pi.inbox"]?.items ?? [];
1086
+ return items.flatMap((raw) => {
1087
+ const input = queuedUserInput(raw);
1088
+ if (!input)
1089
+ return [];
1090
+ const { text, ...rest } = input;
1091
+ return [{ ...rest, preview: clip(text) }];
1092
+ });
1093
+ }
1094
+ var QUEUED_PREVIEW_CHARS = 300, textOf = (content) => {
1095
+ if (typeof content === "string")
1096
+ return content;
1097
+ if (!Array.isArray(content))
1098
+ return "";
1099
+ return content.flatMap((b) => b && b.type === "text" && typeof b.text === "string" ? [b.text] : []).join("");
1100
+ }, clip = (text) => {
1101
+ const chars = [...text.replace(/\s+/g, " ").trim()];
1102
+ return chars.length > QUEUED_PREVIEW_CHARS ? `${chars.slice(0, QUEUED_PREVIEW_CHARS - 1).join("")}\u2026` : chars.join("");
1103
+ };
1104
+ var init_queued = __esm(() => {
1105
+ init_image_prompt();
1106
+ init_input();
1107
+ });
1108
+
868
1109
  // packages/core/src/workspace.ts
869
1110
  import { accessSync, constants, realpathSync, statSync } from "fs";
870
1111
  import { isAbsolute, resolve as resolve2 } from "path";
@@ -1184,6 +1425,7 @@ function projectChatView(view, modelLabel, options = {}) {
1184
1425
  },
1185
1426
  cwd: savedWorkspace(agent?.cwd),
1186
1427
  busy: live.run !== undefined,
1428
+ queued: projectQueued(view),
1187
1429
  status,
1188
1430
  usage: {
1189
1431
  input: usage.input,
@@ -1296,6 +1538,7 @@ var WRITE_DIFF_MAX_LINES = 2000, WRITE_DIFF_MAX_BYTES;
1296
1538
  var init_projection = __esm(() => {
1297
1539
  init_image_prompt();
1298
1540
  init_input();
1541
+ init_queued();
1299
1542
  init_workspace();
1300
1543
  WRITE_DIFF_MAX_BYTES = 64 * 1024;
1301
1544
  });
@@ -1517,6 +1760,132 @@ async function readCompaction(harness, storage, id, context) {
1517
1760
  }
1518
1761
  var init_compaction = () => {};
1519
1762
 
1763
+ // packages/core/src/context-pruning.ts
1764
+ function estimateTokens(text) {
1765
+ const cjk = text.match(CJK)?.length ?? 0;
1766
+ return cjk * TOKENS_PER_CJK_CHAR + (text.length - cjk) * TOKENS_PER_LATIN_CHAR;
1767
+ }
1768
+ function toolResultChars(message) {
1769
+ return resultTexts(message).reduce((total, text) => total + text.length, 0);
1770
+ }
1771
+ function toolResultTokens(message) {
1772
+ return resultTexts(message).reduce((total, text) => total + estimateTokens(text), 0);
1773
+ }
1774
+ function prunedPlaceholder(message, chars) {
1775
+ const { details: _details, ...rest } = message;
1776
+ return {
1777
+ ...rest,
1778
+ content: [
1779
+ {
1780
+ type: "text",
1781
+ text: `[Output of ${message.toolName} omitted from context to save space (${chars} characters). Run the tool again if the content is needed.]`
1782
+ }
1783
+ ]
1784
+ };
1785
+ }
1786
+ function planPrune(entries, settings) {
1787
+ const replaced = new Set;
1788
+ for (const entry of entries)
1789
+ for (const edit of entry.edits ?? [])
1790
+ replaced.add(edit.target);
1791
+ const results = entries.filter(isToolResult);
1792
+ const candidates = results.slice(0, Math.max(0, results.length - settings.keepRecent));
1793
+ const edits = [];
1794
+ const targets = [];
1795
+ let savedChars = 0;
1796
+ let savedTokens = 0;
1797
+ for (const entry of candidates) {
1798
+ if (replaced.has(entry.id))
1799
+ continue;
1800
+ const message = entry.model[0];
1801
+ const chars = toolResultChars(message);
1802
+ if (chars < settings.minChars)
1803
+ continue;
1804
+ const replacement = prunedPlaceholder(message, chars);
1805
+ edits.push({ target: entry.id, action: "replace", messages: [replacement] });
1806
+ targets.push(entry.id);
1807
+ savedChars += chars;
1808
+ savedTokens += toolResultTokens(message);
1809
+ }
1810
+ return edits.length === 0 ? undefined : { edits, targets, savedChars, savedTokens };
1811
+ }
1812
+
1813
+ class ContextPruner {
1814
+ #options;
1815
+ #unsubscribe;
1816
+ #tail = Promise.resolve();
1817
+ #closed = false;
1818
+ constructor(options) {
1819
+ this.#options = options;
1820
+ this.#unsubscribe = options.harness.subscribeCommits(({ changes }) => {
1821
+ if (!options.settings().enabled)
1822
+ return;
1823
+ for (const change of changes) {
1824
+ if (change.type !== "entry" || change.value.kind !== "pi.assistant")
1825
+ continue;
1826
+ const message = change.value.model?.[0];
1827
+ if (message?.role !== "assistant" || message.stopReason === "aborted" || message.stopReason === "error")
1828
+ continue;
1829
+ const usage = message.usage;
1830
+ const tokens = usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
1831
+ if (tokens <= 0)
1832
+ continue;
1833
+ const { conversationId, id } = change.value;
1834
+ this.#enqueue(() => this.#evaluate(conversationId, id, tokens));
1835
+ }
1836
+ });
1837
+ }
1838
+ async flush() {
1839
+ await this.#tail;
1840
+ }
1841
+ close() {
1842
+ this.#closed = true;
1843
+ this.#unsubscribe();
1844
+ }
1845
+ #enqueue(work) {
1846
+ if (this.#closed)
1847
+ return;
1848
+ this.#tail = this.#tail.then(work).catch((error) => {
1849
+ if (!this.#closed)
1850
+ this.#options.report(error);
1851
+ });
1852
+ }
1853
+ async#evaluate(conversationId, entryId, tokens) {
1854
+ const { harness, context } = this.#options;
1855
+ const settings = this.#options.settings();
1856
+ if (!settings.enabled)
1857
+ return;
1858
+ const window2 = await this.#options.windowOf(conversationId);
1859
+ if (window2 <= 0 || tokens / window2 * 100 < settings.triggerPercent)
1860
+ return;
1861
+ const conversation = await harness.conversation(conversationId, context);
1862
+ if (!conversation)
1863
+ return;
1864
+ const view = await conversation.context(context);
1865
+ const plan = planPrune(view.entries, settings);
1866
+ if (!plan)
1867
+ return;
1868
+ if (plan.savedTokens / window2 * 100 < settings.minSavePercent)
1869
+ return;
1870
+ await conversation.submit({
1871
+ type: "write",
1872
+ requestId: `prune:${entryId}`,
1873
+ entry: {
1874
+ kind: PRUNE_ENTRY_KIND,
1875
+ data: { targets: [...plan.targets], savedChars: plan.savedChars },
1876
+ edits: plan.edits
1877
+ }
1878
+ }, context);
1879
+ }
1880
+ }
1881
+ var PRUNE_ENTRY_KIND = "app.neko.context-prune", TOKENS_PER_LATIN_CHAR = 0.3, TOKENS_PER_CJK_CHAR = 0.6, CJK, isToolResult = (entry) => {
1882
+ const message = entry.model?.[0];
1883
+ return entry.kind === "pi.tool-result" && message?.role === "toolResult";
1884
+ }, resultTexts = (message) => message.content.flatMap((block) => block.type === "text" ? [block.text] : []);
1885
+ var init_context_pruning = __esm(() => {
1886
+ CJK = /[\u3000-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]/g;
1887
+ });
1888
+
1520
1889
  // packages/core/src/image-metadata.ts
1521
1890
  import { defineExtension as defineExtension2, hook, ToolTask } from "@earendil-works/pi-durable";
1522
1891
  import sharp from "sharp";
@@ -2176,7 +2545,7 @@ class JobRegistry {
2176
2545
  function reportText(job, tail) {
2177
2546
  const body = reportTail(tail);
2178
2547
  return [
2179
- `[Background job ${job.jobId} "${job.title}" ${describe(job)}]`,
2548
+ `[Background job ${job.jobId} "${job.title}" ${describe2(job)}]`,
2180
2549
  `Command: ${job.command}`,
2181
2550
  body ? `Last output (use the job tool's output action with jobId "${job.jobId}" for more):
2182
2551
  ${body}` : "It produced no output."
@@ -2355,7 +2724,7 @@ class JobManager {
2355
2724
  if (kind === "stop") {
2356
2725
  const jobId = stringField(args.jobId, "jobId");
2357
2726
  const job = await this.stop(io, api.conversationId, jobId, "model", context);
2358
- return this.#text(`Job ${job.jobId} ${describe(job)}.`);
2727
+ return this.#text(`Job ${job.jobId} ${describe2(job)}.`);
2359
2728
  }
2360
2729
  if (kind !== "start")
2361
2730
  throw new Error("Invalid job arguments: action must be start, output, stop or list.");
@@ -2383,7 +2752,7 @@ class JobManager {
2383
2752
  #listText(items) {
2384
2753
  if (!items.length)
2385
2754
  return "No jobs.";
2386
- return items.map((job) => `${job.jobId} [${describe(job)}] "${job.title}" \u2014 ${job.command}`).join(`
2755
+ return items.map((job) => `${job.jobId} [${describe2(job)}] "${job.title}" \u2014 ${job.command}`).join(`
2387
2756
  `);
2388
2757
  }
2389
2758
  async start(io, conversationId, cwd, args, context) {
@@ -2435,7 +2804,7 @@ class JobManager {
2435
2804
  return this.#render(job, read.text, read.offset, read.dropped);
2436
2805
  }
2437
2806
  #render(job, text, offset, dropped) {
2438
- const lines = [`Job ${job.jobId} ${describe(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
2807
+ const lines = [`Job ${job.jobId} ${describe2(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
2439
2808
  if (dropped > 0)
2440
2809
  lines.push(`(${dropped} bytes of earlier output were dropped)`);
2441
2810
  lines.push(text ? `Output (next offset ${offset}):
@@ -2500,7 +2869,7 @@ ${text}` : `No new output (next offset ${offset}).`);
2500
2869
  this.registry.abortAll();
2501
2870
  }
2502
2871
  }
2503
- var plain = (value) => JSON.parse(JSON.stringify(value)), statusWord, describe = (job) => {
2872
+ var plain = (value) => JSON.parse(JSON.stringify(value)), statusWord, describe2 = (job) => {
2504
2873
  const parts = [statusWord[job.status]];
2505
2874
  if (job.exitCode !== null)
2506
2875
  parts.push(`exit code ${job.exitCode}`);
@@ -2886,6 +3255,14 @@ var createResourceSnapshots = (load, submission) => {
2886
3255
  discard(id, requestId) {
2887
3256
  conversations.get(id)?.pending.delete(requestId);
2888
3257
  },
3258
+ move(id, from, to) {
3259
+ const pending = conversations.get(id)?.pending;
3260
+ const snapshot = pending?.get(from);
3261
+ if (!pending || !snapshot)
3262
+ return;
3263
+ pending.delete(from);
3264
+ pending.set(to, snapshot);
3265
+ },
2889
3266
  forget(id) {
2890
3267
  conversations.delete(id);
2891
3268
  }
@@ -3419,21 +3796,22 @@ var init_tool_approvals = __esm(() => {
3419
3796
  });
3420
3797
  });
3421
3798
 
3422
- // packages/core/src/web-search.ts
3423
- import { Type as Type4 } from "@earendil-works/pi-ai";
3424
- import { defineExtension as defineExtension8, defineTool as defineTool4 } from "@earendil-works/pi-durable";
3799
+ // packages/core/src/web-search/http.ts
3425
3800
  function redact(text, key) {
3426
3801
  for (const secret of key ? [key, encodeURIComponent(key)] : [])
3427
3802
  text = text.replaceAll(secret, "[redacted]");
3428
- return text.replace(/tvly-[a-zA-Z0-9_-]+/g, "[redacted]");
3803
+ return text.replace(/\b(?:tvly|sk)-[a-zA-Z0-9_-]+/g, "[redacted]");
3429
3804
  }
3430
3805
  function record(value) {
3431
3806
  return typeof value === "object" && value !== null && !Array.isArray(value);
3432
3807
  }
3433
- async function readBounded(response, signal) {
3808
+ function truncate(text, max) {
3809
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}\u2026` : text;
3810
+ }
3811
+ async function readBounded(response, signal, label) {
3434
3812
  if (Number(response.headers.get("content-length")) > MAX_RESPONSE_BYTES) {
3435
3813
  await response.body?.cancel();
3436
- throw new TavilyError("Tavily response exceeds the 2 MiB limit.");
3814
+ throw new WebToolError(`${label} response exceeds the 2 MiB limit.`);
3437
3815
  }
3438
3816
  if (!response.body)
3439
3817
  return "";
@@ -3454,7 +3832,7 @@ async function readBounded(response, signal) {
3454
3832
  return text + decoder.decode();
3455
3833
  size += chunk.value.byteLength;
3456
3834
  if (size > MAX_RESPONSE_BYTES)
3457
- throw new TavilyError("Tavily response exceeds the 2 MiB limit.");
3835
+ throw new WebToolError(`${label} response exceeds the 2 MiB limit.`);
3458
3836
  text += decoder.decode(chunk.value, { stream: true });
3459
3837
  }
3460
3838
  } finally {
@@ -3463,7 +3841,8 @@ async function readBounded(response, signal) {
3463
3841
  reader.releaseLock();
3464
3842
  }
3465
3843
  }
3466
- async function tavilyPost(url, body, resolveKey, fetchImpl, context) {
3844
+ async function postJson(options) {
3845
+ const { label, context } = options;
3467
3846
  let key;
3468
3847
  const signal = AbortSignal.any([
3469
3848
  AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -3471,68 +3850,190 @@ async function tavilyPost(url, body, resolveKey, fetchImpl, context) {
3471
3850
  ]);
3472
3851
  try {
3473
3852
  signal.throwIfAborted();
3474
- key = await resolveKey();
3853
+ key = await options.resolveKey();
3475
3854
  signal.throwIfAborted();
3476
- const headers = { "content-type": "application/json" };
3477
- if (key)
3478
- headers.authorization = `Bearer ${key}`;
3479
- else
3480
- headers["x-tavily-access-mode"] = "keyless";
3481
- const response = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal });
3482
- const text = redact(await readBounded(response, signal), key);
3855
+ const response = await options.fetchImpl(options.url, {
3856
+ method: "POST",
3857
+ headers: { "content-type": "application/json", ...options.headers(key) },
3858
+ body: JSON.stringify(options.body),
3859
+ signal
3860
+ });
3861
+ const text = redact(await readBounded(response, signal, label), key);
3483
3862
  if (!response.ok)
3484
- throw new TavilyError(`Tavily request failed (${response.status}): ${truncate(text, 500)}${key ? "" : " (keyless rate limits apply; save a Tavily API key in settings for higher limits)"}`);
3485
- const data = JSON.parse(text);
3486
- if (!record(data))
3487
- throw new TavilyError("Invalid Tavily response.");
3488
- if (data.error)
3489
- throw new TavilyError(`Tavily request failed: ${truncate(JSON.stringify(data.error), 500)}`);
3490
- return data;
3863
+ throw options.httpError(response.status, text, key);
3864
+ return JSON.parse(text);
3491
3865
  } catch (error) {
3492
3866
  if (context.abortSignal?.aborted)
3493
- throw new TavilyError("Web request cancelled.");
3867
+ throw new WebToolError("Web request cancelled.");
3494
3868
  if (signal.aborted)
3495
- throw new TavilyError("Tavily request timed out. Please retry.");
3496
- if (error instanceof TavilyError)
3869
+ throw new WebToolError(`${label} request timed out. Please retry.`);
3870
+ if (error instanceof WebToolError)
3497
3871
  throw error;
3498
- throw new TavilyError("Tavily request failed. Check the connection and credentials, then retry.");
3872
+ throw new WebToolError(`${label} request failed. Check the connection and credentials, then retry.`);
3499
3873
  }
3500
3874
  }
3875
+ var REQUEST_TIMEOUT_MS = 30000, MAX_RESPONSE_BYTES, WebToolError;
3876
+ var init_http = __esm(() => {
3877
+ MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
3878
+ WebToolError = class WebToolError extends Error {
3879
+ status;
3880
+ constructor(message, status) {
3881
+ super(message);
3882
+ this.status = status;
3883
+ }
3884
+ };
3885
+ });
3886
+
3887
+ // packages/core/src/web-search/deepseek.ts
3888
+ function defaultSleep(ms, signal) {
3889
+ return new Promise((resolve, reject) => {
3890
+ if (signal?.aborted)
3891
+ return reject(signal.reason);
3892
+ const timer = setTimeout(resolve, ms);
3893
+ signal?.addEventListener("abort", () => {
3894
+ clearTimeout(timer);
3895
+ reject(signal.reason);
3896
+ }, { once: true });
3897
+ });
3898
+ }
3899
+ function apiMessage(text) {
3900
+ try {
3901
+ const data = JSON.parse(text);
3902
+ if (record(data) && record(data.error) && typeof data.error.message === "string")
3903
+ return truncate(data.error.message, 300);
3904
+ } catch {}
3905
+ return truncate(text, 300);
3906
+ }
3907
+ function httpError(status, text) {
3908
+ const guidance = "Check the key in /login or the Web settings, or switch the search provider.";
3909
+ if (status === 401)
3910
+ return new WebToolError(`DeepSeek API key is missing or invalid. ${guidance}`, status);
3911
+ if (status === 402)
3912
+ return new WebToolError("DeepSeek account balance is insufficient. Recharge, or switch the search provider to Tavily.", status);
3913
+ if (status === 403)
3914
+ return new WebToolError("DeepSeek blocked this request by its security policy.", status);
3915
+ if (status === 404)
3916
+ return new WebToolError("DeepSeek could not fetch a usable page for this URL.", status);
3917
+ if (status === 429)
3918
+ return new WebToolError("DeepSeek rate limit reached. Retry shortly.", status);
3919
+ if (status === 408 || status >= 500)
3920
+ return new WebToolError(`DeepSeek service error (${status}). Retry shortly.`, status);
3921
+ return new WebToolError(`DeepSeek request failed (${status}): ${apiMessage(text)}`, status);
3922
+ }
3923
+ function dateOnly(value) {
3924
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : undefined;
3925
+ }
3926
+ function createDeepseekProvider(resolveKey, fetchImpl = fetch, options = {}) {
3927
+ const sleep = options.sleep ?? defaultSleep;
3928
+ const requireKey = async () => {
3929
+ const key = await resolveKey();
3930
+ if (!key)
3931
+ throw new WebToolError(DEEPSEEK_MISSING_KEY);
3932
+ return key;
3933
+ };
3934
+ const post = (url, body, context) => postJson({
3935
+ label: "DeepSeek",
3936
+ url,
3937
+ body,
3938
+ fetchImpl,
3939
+ context,
3940
+ resolveKey: requireKey,
3941
+ headers: (key) => ({ authorization: `Bearer ${key}` }),
3942
+ httpError
3943
+ });
3944
+ return {
3945
+ id: "deepseek",
3946
+ async search(query, limit, context) {
3947
+ if (!query.trim())
3948
+ throw new WebToolError("Provide a non-empty query.");
3949
+ if ([...query].length > DEEPSEEK_QUERY_MAX_CHARS)
3950
+ throw new WebToolError(`DeepSeek search queries are limited to ${DEEPSEEK_QUERY_MAX_CHARS} characters. Shorten the query and retry.`);
3951
+ await requireKey();
3952
+ const data = await post(SEARCH_URL, { query, count: limit, need_content: false }, context);
3953
+ if (!record(data) || !Array.isArray(data.results) || !data.results.every(record))
3954
+ throw new WebToolError("Invalid DeepSeek response.");
3955
+ return data.results.slice(0, limit).map((item) => {
3956
+ const date = dateOnly(item.date);
3957
+ return {
3958
+ title: String(item.title ?? "(untitled)"),
3959
+ url: String(item.url ?? ""),
3960
+ text: String(item.summary || item.snippet || ""),
3961
+ ...date ? { date } : {}
3962
+ };
3963
+ });
3964
+ },
3965
+ async extract(urls, context) {
3966
+ await requireKey();
3967
+ const outcomes = new Array(urls.length);
3968
+ let next = 0;
3969
+ let fatal;
3970
+ const fetchOne = async (url) => {
3971
+ for (let attempt = 0;; attempt++) {
3972
+ try {
3973
+ const data = await post(FETCH_URL, { url }, context);
3974
+ if (!record(data) || typeof data.content !== "string")
3975
+ throw new WebToolError("Invalid DeepSeek response.");
3976
+ return { url, text: data.content };
3977
+ } catch (error) {
3978
+ const failure = error instanceof WebToolError ? error : new WebToolError("DeepSeek request failed.");
3979
+ if (context.abortSignal?.aborted || wholeCall(failure.status))
3980
+ throw failure;
3981
+ if (!retryable(failure.status) || attempt >= MAX_RETRIES)
3982
+ return { url, error: failure.message };
3983
+ try {
3984
+ await sleep(BACKOFF_MS[attempt] ?? 750, context.abortSignal);
3985
+ } catch {
3986
+ throw new WebToolError("Web request cancelled.");
3987
+ }
3988
+ }
3989
+ }
3990
+ };
3991
+ const worker = async () => {
3992
+ while (!fatal) {
3993
+ const index = next++;
3994
+ if (index >= urls.length)
3995
+ return;
3996
+ try {
3997
+ outcomes[index] = await fetchOne(urls[index]);
3998
+ } catch (error) {
3999
+ fatal ??= error instanceof WebToolError ? error : new WebToolError("DeepSeek request failed.");
4000
+ }
4001
+ }
4002
+ };
4003
+ await Promise.all(Array.from({ length: Math.min(FETCH_CONCURRENCY, urls.length) }, worker));
4004
+ if (fatal)
4005
+ throw fatal;
4006
+ return outcomes;
4007
+ }
4008
+ };
4009
+ }
4010
+ var SEARCH_URL = "https://api.deepseek.com/v1/tools/search", FETCH_URL = "https://api.deepseek.com/v1/tools/fetch", DEEPSEEK_QUERY_MAX_CHARS = 128, FETCH_CONCURRENCY = 3, MAX_RETRIES = 2, BACKOFF_MS, DEEPSEEK_MISSING_KEY = "DeepSeek API key is not configured. Save one with /login or in the Web settings, or switch the search provider to Tavily.", retryable = (status) => status !== undefined && (status === 429 || status === 408 || status >= 500), wholeCall = (status) => status === 401 || status === 402;
4011
+ var init_deepseek = __esm(() => {
4012
+ init_http();
4013
+ BACKOFF_MS = [250, 750];
4014
+ });
4015
+
4016
+ // packages/core/src/web-search/index.ts
4017
+ import { Type as Type4 } from "@earendil-works/pi-ai";
4018
+ import { defineExtension as defineExtension8, defineTool as defineTool4 } from "@earendil-works/pi-durable";
3501
4019
  function toolError(error) {
3502
4020
  return {
3503
4021
  output: [
3504
- { type: "text", text: error instanceof TavilyError ? error.message : "Web tool failed. Please retry." }
4022
+ { type: "text", text: error instanceof WebToolError ? error.message : "Web tool failed. Please retry." }
3505
4023
  ],
3506
4024
  isError: true
3507
4025
  };
3508
4026
  }
3509
- function truncate(text, max) {
3510
- return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}\u2026` : text;
3511
- }
3512
- function rows(data, field) {
3513
- const value = data[field];
3514
- if (!Array.isArray(value) || !value.every(record))
3515
- throw new TavilyError("Invalid Tavily response.");
3516
- return value;
3517
- }
3518
4027
  function content(text) {
3519
4028
  return { output: [{ type: "text", text }] };
3520
4029
  }
3521
- function urlKey(value) {
3522
- const text = String(value);
3523
- try {
3524
- return new URL(text).href;
3525
- } catch {
3526
- return text;
3527
- }
3528
- }
3529
- function createWebSearchTools(resolveKey, fetchImpl = fetch) {
4030
+ function createWebSearchTools(resolveProvider) {
3530
4031
  return defineExtension8({
3531
4032
  name: "neko-web-search",
3532
4033
  tools: [
3533
4034
  defineTool4({
3534
4035
  name: "web_search",
3535
- 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.",
4036
+ description: "Search the live web for current or unknown information. Returns ranked sources (title, URL, bounded excerpts). Keep the query short and focused (ideally under 128 characters). Use only when up-to-date or external information is needed; use web_extract to read a known URL.",
3536
4037
  parameters: Type4.Object({
3537
4038
  query: Type4.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
3538
4039
  maxResults: Type4.Optional(Type4.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
@@ -3540,18 +4041,19 @@ function createWebSearchTools(resolveKey, fetchImpl = fetch) {
3540
4041
  async execute({ query, maxResults }, _api, context) {
3541
4042
  try {
3542
4043
  if (!query.trim() || query.length > 4096)
3543
- throw new TavilyError("Provide a non-empty query of at most 4096 characters.");
4044
+ throw new WebToolError("Provide a non-empty query of at most 4096 characters.");
3544
4045
  if (maxResults !== undefined && !Number.isFinite(maxResults))
3545
- throw new TavilyError("maxResults must be a finite number.");
4046
+ throw new WebToolError("maxResults must be a finite number.");
3546
4047
  const limit = Math.max(1, Math.min(MAX_RESULTS, Math.trunc(maxResults ?? DEFAULT_RESULTS)));
3547
- const data = await tavilyPost(SEARCH_URL, { query, max_results: limit, search_depth: "basic" }, resolveKey, fetchImpl, context);
3548
- const results = rows(data, "results").slice(0, limit);
4048
+ const provider = await resolveProvider(context);
4049
+ const results = (await provider.search(query, limit, context)).slice(0, limit);
3549
4050
  const budget = Math.floor(MAX_TOTAL_CHARS / Math.max(1, results.length)) - 2;
3550
4051
  return content(results.map((item, index) => {
3551
- const header = `${index + 1}. ${truncate(String(item.title ?? "(untitled)"), 200)}
3552
- ${truncate(String(item.url ?? ""), 300)}
3553
- `;
3554
- return header + truncate(String(item.content ?? ""), Math.min(MAX_SOURCE_CHARS, budget - header.length));
4052
+ const header = `${index + 1}. ${truncate(item.title, 200)}
4053
+ ${truncate(item.url, 300)}
4054
+ ${item.date ? `Published: ${item.date}
4055
+ ` : ""}`;
4056
+ return header + truncate(item.text, Math.min(MAX_SOURCE_CHARS, budget - header.length));
3555
4057
  }).join(`
3556
4058
 
3557
4059
  `) || "No results.");
@@ -3573,28 +4075,25 @@ ${truncate(String(item.url ?? ""), 300)}
3573
4075
  async execute({ urls }, _api, context) {
3574
4076
  try {
3575
4077
  if (!urls.length || urls.length > MAX_URLS)
3576
- throw new TavilyError(`Provide between 1 and ${MAX_URLS} URLs.`);
4078
+ throw new WebToolError(`Provide between 1 and ${MAX_URLS} URLs.`);
3577
4079
  for (const url of urls) {
3578
4080
  let parsed;
3579
4081
  try {
3580
4082
  parsed = new URL(url);
3581
4083
  } catch {
3582
- throw new TavilyError("Provide valid http(s) URLs.");
4084
+ throw new WebToolError("Provide valid http(s) URLs.");
3583
4085
  }
3584
4086
  if (url.length > 2048 || !["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password)
3585
- throw new TavilyError("Provide http(s) URLs of at most 2048 characters without embedded credentials.");
4087
+ throw new WebToolError("Provide http(s) URLs of at most 2048 characters without embedded credentials.");
3586
4088
  }
3587
- const data = await tavilyPost(EXTRACT_URL, { urls, format: "markdown" }, resolveKey, fetchImpl, context);
3588
- const results = rows(data, "results");
3589
- const failed = data.failed_results === undefined ? [] : rows(data, "failed_results");
3590
- const byUrl = (items) => new Map(items.map((item) => [urlKey(item.url), item]));
3591
- const successes = byUrl(results), failures = byUrl(failed);
4089
+ const provider = await resolveProvider(context);
4090
+ const outcomes = await provider.extract(urls, context);
3592
4091
  const budget = Math.floor(MAX_TOTAL_CHARS / urls.length) - 2;
3593
4092
  return content(urls.map((url, index) => {
3594
4093
  const header = `## ${index + 1}. ${truncate(url, 300)}
3595
4094
  `;
3596
- const result = successes.get(urlKey(url)), failure = failures.get(urlKey(url));
3597
- const text = result ? String(result.raw_content ?? "") : `Extraction failed: ${String(failure?.error ?? "No result returned for this URL")}`;
4095
+ const outcome = outcomes[index];
4096
+ const text = outcome && "text" in outcome ? outcome.text : `Extraction failed: ${outcome?.error ?? "No result returned for this URL"}`;
3598
4097
  return header + truncate(text, Math.min(MAX_SOURCE_CHARS, budget - header.length));
3599
4098
  }).join(`
3600
4099
 
@@ -3607,11 +4106,75 @@ ${truncate(String(item.url ?? ""), 300)}
3607
4106
  ]
3608
4107
  });
3609
4108
  }
3610
- var SEARCH_URL = "https://api.tavily.com/search", EXTRACT_URL = "https://api.tavily.com/extract", REQUEST_TIMEOUT_MS = 30000, MAX_RESPONSE_BYTES, MAX_URLS = 20, MAX_RESULTS = 10, DEFAULT_RESULTS = 5, MAX_SOURCE_CHARS = 2000, MAX_TOTAL_CHARS = 12000, TavilyError;
4109
+ var MAX_URLS = 20, MAX_RESULTS = 10, DEFAULT_RESULTS = 5, MAX_SOURCE_CHARS = 2000, MAX_TOTAL_CHARS = 12000;
3611
4110
  var init_web_search = __esm(() => {
3612
- MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
3613
- TavilyError = class TavilyError extends Error {
4111
+ init_http();
4112
+ init_http();
4113
+ });
4114
+
4115
+ // packages/core/src/web-search/tavily.ts
4116
+ function rows(data, field) {
4117
+ const value = data[field];
4118
+ if (!Array.isArray(value) || !value.every(record))
4119
+ throw new WebToolError("Invalid Tavily response.");
4120
+ return value;
4121
+ }
4122
+ function urlKey(value) {
4123
+ const text = String(value);
4124
+ try {
4125
+ return new URL(text).href;
4126
+ } catch {
4127
+ return text;
4128
+ }
4129
+ }
4130
+ function createTavilyProvider(resolveKey, fetchImpl = fetch) {
4131
+ const post = async (url, body, context) => {
4132
+ const data = await postJson({
4133
+ label: "Tavily",
4134
+ url,
4135
+ body,
4136
+ fetchImpl,
4137
+ context,
4138
+ resolveKey,
4139
+ headers: (key) => key ? { authorization: `Bearer ${key}` } : { "x-tavily-access-mode": "keyless" },
4140
+ httpError: (status, text, key) => new WebToolError(`Tavily request failed (${status}): ${truncate(text, 500)}${key ? "" : " (keyless rate limits apply; save a Tavily API key in settings for higher limits)"}`, status)
4141
+ });
4142
+ if (!record(data))
4143
+ throw new WebToolError("Invalid Tavily response.");
4144
+ if (data.error)
4145
+ throw new WebToolError(`Tavily request failed: ${truncate(JSON.stringify(data.error), 500)}`);
4146
+ return data;
3614
4147
  };
4148
+ return {
4149
+ id: "tavily",
4150
+ async search(query, limit, context) {
4151
+ const data = await post(SEARCH_URL2, { query, max_results: limit, search_depth: "basic" }, context);
4152
+ return rows(data, "results").slice(0, limit).map((item) => ({
4153
+ title: String(item.title ?? "(untitled)"),
4154
+ url: String(item.url ?? ""),
4155
+ text: String(item.content ?? "")
4156
+ }));
4157
+ },
4158
+ async extract(urls, context) {
4159
+ const data = await post(EXTRACT_URL, { urls, format: "markdown" }, context);
4160
+ const results = rows(data, "results");
4161
+ const failed = data.failed_results === undefined ? [] : rows(data, "failed_results");
4162
+ const byUrl = (items) => new Map(items.map((item) => [urlKey(item.url), item]));
4163
+ const successes = byUrl(results);
4164
+ const failures = byUrl(failed);
4165
+ return urls.map((url) => {
4166
+ const result = successes.get(urlKey(url));
4167
+ if (result)
4168
+ return { url, text: String(result.raw_content ?? "") };
4169
+ const failure = failures.get(urlKey(url));
4170
+ return { url, error: String(failure?.error ?? "No result returned for this URL") };
4171
+ });
4172
+ }
4173
+ };
4174
+ }
4175
+ var SEARCH_URL2 = "https://api.tavily.com/search", EXTRACT_URL = "https://api.tavily.com/extract";
4176
+ var init_tavily = __esm(() => {
4177
+ init_http();
3615
4178
  });
3616
4179
 
3617
4180
  // packages/core/src/workspace-files.ts
@@ -3988,6 +4551,7 @@ import {
3988
4551
  createRegistry,
3989
4552
  defineDoc as defineDoc5,
3990
4553
  Harness,
4554
+ InboxDoc as InboxDoc2,
3991
4555
  LiveDoc as importLiveDoc,
3992
4556
  MemoryStorage,
3993
4557
  UsageDoc as UsageDoc2,
@@ -4034,12 +4598,21 @@ async function createNekoRuntime(options = {}) {
4034
4598
  registry.install(createCodingTools({ images: await createNodePhotonImages() }));
4035
4599
  registry.install(ImageDetails);
4036
4600
  registry.install(AgentFileSearch);
4037
- registry.install(createWebSearchTools(async () => {
4601
+ const tavily = createTavilyProvider(async () => {
4038
4602
  const saved = await effectiveCredentials.read("tavily");
4039
4603
  if (saved?.type === "api_key" && saved.key)
4040
4604
  return saved.key;
4041
4605
  return process.env.TAVILY_API_KEY || undefined;
4042
- }));
4606
+ });
4607
+ const deepseekApiKey = async () => {
4608
+ const saved = await effectiveCredentials.read("deepseek");
4609
+ if (saved?.type === "api_key" && saved.key)
4610
+ return saved.key;
4611
+ return process.env.DEEPSEEK_API_KEY || undefined;
4612
+ };
4613
+ const deepseek = createDeepseekProvider(deepseekApiKey);
4614
+ let searchProvider = options.searchProvider ?? DEFAULT_SEARCH_PROVIDER;
4615
+ registry.install(createWebSearchTools(async () => searchProvider === "deepseek" ? deepseek : tavily));
4043
4616
  const loadResources = createResourceLoader(options.promptHome);
4044
4617
  const resourceSnapshots = createResourceSnapshots(loadResources, (id, requestId) => storage.submissionByRequest(id, requestId, context));
4045
4618
  registry.install(createNekoPrompt(resourceSnapshots.current));
@@ -4053,9 +4626,16 @@ async function createNekoRuntime(options = {}) {
4053
4626
  let approvals;
4054
4627
  const approvalExtension = createApprovalExtension(() => approvals);
4055
4628
  registry.install(approvalExtension);
4629
+ let savedContext = emptySavedContextSettings();
4630
+ let contextState = resolveContextSettings(options.contextSettings, savedContext);
4056
4631
  const harness = await Harness.open(storage, {
4057
4632
  models,
4058
4633
  registry,
4634
+ settings: {
4635
+ get compaction() {
4636
+ return compactionPolicy(contextState, models.getModels().map((model) => model.contextWindow));
4637
+ }
4638
+ },
4059
4639
  onReport: options.onReport,
4060
4640
  conversationCreated: async (tx, record) => {
4061
4641
  (await tx.doc(SessionMetadataDoc, record.id)).createdAt = Date.now();
@@ -4097,6 +4677,21 @@ async function createNekoRuntime(options = {}) {
4097
4677
  cwd: defaultCwd
4098
4678
  }
4099
4679
  });
4680
+ const savedSearch = (await harness.snapshot(SearchSettingsDoc, conversation.id, context))?.provider;
4681
+ if (isSearchProvider(savedSearch))
4682
+ searchProvider = savedSearch;
4683
+ savedContext = sanitizeSavedContextSettings(await harness.snapshot(ContextSettingsDoc, conversation.id, context));
4684
+ contextState = resolveContextSettings(options.contextSettings, savedContext);
4685
+ const persistContext = async (next) => {
4686
+ await harness.commit(async (tx) => {
4687
+ const doc = await tx.doc(ContextSettingsDoc, conversation.id);
4688
+ doc.compaction = next.compaction;
4689
+ doc.pruning = next.pruning;
4690
+ }, context);
4691
+ savedContext = next;
4692
+ contextState = resolveContextSettings(options.contextSettings, savedContext);
4693
+ return contextState;
4694
+ };
4100
4695
  const registerWorkspace = async (path) => {
4101
4696
  await harness.commit(async (tx) => {
4102
4697
  const doc = await tx.doc(WorkspacesDoc, conversation.id);
@@ -4133,10 +4728,22 @@ async function createNekoRuntime(options = {}) {
4133
4728
  for (const record of existing)
4134
4729
  if (!record.owner && !(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
4135
4730
  await metadata.restore(record.id);
4731
+ const pruner = new ContextPruner({
4732
+ harness,
4733
+ context,
4734
+ settings: () => contextState.effective.pruning,
4735
+ windowOf: async (id) => {
4736
+ const target = await harness.conversation(id, context);
4737
+ const model = (await target?.agent(context))?.model;
4738
+ return model && models.getModel(model.provider, model.modelId)?.contextWindow || 0;
4739
+ },
4740
+ report: (error) => options.onReport?.(error)
4741
+ });
4136
4742
  let closing;
4137
4743
  const cleanup = () => {
4138
4744
  if (!closing)
4139
4745
  closing = (async () => {
4746
+ pruner.close();
4140
4747
  jobs.shutdown();
4141
4748
  await workspaceFiles.close();
4142
4749
  await metadata.close();
@@ -4293,6 +4900,57 @@ async function createNekoRuntime(options = {}) {
4293
4900
  },
4294
4901
  abort() {
4295
4902
  return target.abort(context);
4903
+ },
4904
+ async withdraw(submissionId) {
4905
+ const id = submissionId;
4906
+ const inbox = await harness.snapshot(InboxDoc2, target.id, context);
4907
+ const item = inbox?.items.find((i) => i.id === id);
4908
+ const input = item ? queuedUserInput(item) : undefined;
4909
+ if (item && !input)
4910
+ return { result: "unknown" };
4911
+ const requestId = (await (await harness.submission(id, context))?.status(context))?.requestId;
4912
+ const outcome = await harness.abortSubmission(id, context, target.id);
4913
+ if (outcome !== "aborted") {
4914
+ return { result: outcome === "already_placed" ? "placed" : outcome === "settled" ? "settled" : "unknown" };
4915
+ }
4916
+ if (requestId !== undefined)
4917
+ resourceSnapshots.discard(target.id, requestId);
4918
+ await metadata.reconcile(target.id);
4919
+ return { result: "withdrawn", ...input ? { text: input.text, images: input.images } : {} };
4920
+ },
4921
+ async steer(submissionId) {
4922
+ const id = submissionId;
4923
+ const inbox = await harness.snapshot(InboxDoc2, target.id, context);
4924
+ const item = inbox?.items.find((i) => i.id === id);
4925
+ const input = item ? queuedUserInput(item) : undefined;
4926
+ if (item && (!input || item.mode === "write"))
4927
+ return { result: "unknown" };
4928
+ if (item && input?.mode === "steer")
4929
+ return { result: "steered", id: submissionId };
4930
+ const requestId = (await (await harness.submission(id, context))?.status(context))?.requestId;
4931
+ const outcome = await harness.abortSubmission(id, context, target.id);
4932
+ if (outcome !== "aborted" || !item || !input) {
4933
+ return { result: outcome === "already_placed" ? "placed" : outcome === "settled" ? "settled" : "unknown" };
4934
+ }
4935
+ const steerRequestId = requestId !== undefined ? `steer:${requestId}` : crypto.randomUUID();
4936
+ try {
4937
+ const next = await target.submit({
4938
+ type: "input",
4939
+ content: item.content,
4940
+ ...item.entry ? { entry: item.entry } : {},
4941
+ requestId: steerRequestId,
4942
+ whenBusy: "steer"
4943
+ }, context);
4944
+ if (requestId !== undefined)
4945
+ resourceSnapshots.move(target.id, requestId, steerRequestId);
4946
+ await metadata.reconcile(target.id);
4947
+ return { result: "steered", id: next.id };
4948
+ } catch {
4949
+ if (requestId !== undefined)
4950
+ resourceSnapshots.discard(target.id, requestId);
4951
+ await metadata.reconcile(target.id);
4952
+ return { result: "withdrawn", text: input.text, images: input.images };
4953
+ }
4296
4954
  }
4297
4955
  });
4298
4956
  const rootSession = makeSession(conversation);
@@ -4503,6 +5161,26 @@ async function createNekoRuntime(options = {}) {
4503
5161
  source: runtimeKeys.has(spec.id) ? "runtime" : savedConfigured ? "saved" : environmentConfigured ? "environment" : spec.keyless ? "keyless" : "none"
4504
5162
  };
4505
5163
  },
5164
+ async contextSettings() {
5165
+ return contextState;
5166
+ },
5167
+ async setContextSettings(patch) {
5168
+ return persistContext(applyContextPatch(savedContext, parseContextSettingsPatch(patch)));
5169
+ },
5170
+ async resetContextSettings() {
5171
+ return persistContext(emptySavedContextSettings());
5172
+ },
5173
+ async searchSettings() {
5174
+ return { selected: searchProvider };
5175
+ },
5176
+ async setSearchProvider(provider) {
5177
+ if (!isSearchProvider(provider))
5178
+ throw new Error("Unknown search provider.");
5179
+ await harness.commit(async (tx) => {
5180
+ (await tx.doc(SearchSettingsDoc, conversation.id)).provider = provider;
5181
+ }, context);
5182
+ searchProvider = provider;
5183
+ },
4506
5184
  async saveCredential(provider, key) {
4507
5185
  const spec = credentialProvider(provider);
4508
5186
  if (typeof key !== "string" || !key.trim() || key.length > 4096 || Array.from(key).some((char) => char.charCodeAt(0) < 32))
@@ -4576,10 +5254,12 @@ async function createNekoRuntime(options = {}) {
4576
5254
  }
4577
5255
  };
4578
5256
  }
4579
- var DEFAULT_MODEL, DEFAULT_STORAGE_PATH, ArchivedDoc, WorkspacesDoc, ActivityDoc;
5257
+ var DEFAULT_MODEL, DEFAULT_STORAGE_PATH, ArchivedDoc, WorkspacesDoc, SearchSettingsDoc, ContextSettingsDoc, ActivityDoc;
4580
5258
  var init_runtime = __esm(() => {
4581
5259
  init_agent_file_search();
4582
5260
  init_compaction();
5261
+ init_context_pruning();
5262
+ init_context_settings();
4583
5263
  init_credentials();
4584
5264
  init_image_metadata();
4585
5265
  init_image_prompt();
@@ -4591,11 +5271,15 @@ var init_runtime = __esm(() => {
4591
5271
  init_invoke();
4592
5272
  init_prompt();
4593
5273
  init_resources();
5274
+ init_queued();
5275
+ init_search_providers();
4594
5276
  init_session_metadata();
4595
5277
  init_subagent_usage();
4596
5278
  init_subagents();
4597
5279
  init_tool_approvals();
5280
+ init_deepseek();
4598
5281
  init_web_search();
5282
+ init_tavily();
4599
5283
  init_workspace();
4600
5284
  init_workspace_files();
4601
5285
  DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
@@ -4616,6 +5300,22 @@ var init_runtime = __esm(() => {
4616
5300
  fork: "initial",
4617
5301
  initial: () => ({ items: [] })
4618
5302
  });
5303
+ SearchSettingsDoc = defineDoc5({
5304
+ kind: "app.neko.search-settings",
5305
+ version: 1,
5306
+ scope: "conversation",
5307
+ history: "latest",
5308
+ fork: "initial",
5309
+ initial: () => ({ provider: null })
5310
+ });
5311
+ ContextSettingsDoc = defineDoc5({
5312
+ kind: "app.neko.context-settings",
5313
+ version: 1,
5314
+ scope: "conversation",
5315
+ history: "latest",
5316
+ fork: "initial",
5317
+ initial: emptySavedContextSettings
5318
+ });
4619
5319
  ActivityDoc = defineDoc5({
4620
5320
  kind: "app.neko.activity",
4621
5321
  version: 1,
@@ -4641,12 +5341,15 @@ function mergeTranscriptItems(previous, incoming) {
4641
5341
  // packages/core/src/index.ts
4642
5342
  var init_src = __esm(() => {
4643
5343
  init_config();
5344
+ init_context_settings();
4644
5345
  init_credentials();
4645
5346
  init_images();
4646
5347
  init_jobs();
4647
5348
  init_projection();
4648
5349
  init_frontmatter();
5350
+ init_queued();
4649
5351
  init_runtime();
5352
+ init_search_providers();
4650
5353
  init_session_metadata();
4651
5354
  init_subagent_usage();
4652
5355
  init_subagents();
@@ -4763,7 +5466,7 @@ function relativeTime(at, now, language = "en") {
4763
5466
  var sessionName = (session, empty = "New session") => session.title || session.firstPromptPreview || empty, messageTime = (session) => session.lastMessageAt ?? session.createdAt, byMessage = (a, b) => messageTime(b) - messageTime(a) || Number(b.id) - Number(a.id), byVisited = (a, b) => b.lastUsedAt - a.lastUsedAt || Number(b.id) - Number(a.id);
4764
5467
 
4765
5468
  // packages/gateway/src/contract.ts
4766
- import { method, remote } from "@earendil-works/chord";
5469
+ import { hostNamed, method, remote } from "@earendil-works/chord";
4767
5470
  import { Type as Type5 } from "typebox";
4768
5471
  function contract(name, def) {
4769
5472
  const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
@@ -4773,9 +5476,9 @@ function contract(name, def) {
4773
5476
  methods: Object.fromEntries(Object.entries(value.methods ?? {}).map(([member, [args, result]]) => [member, method(args, result)]))
4774
5477
  }
4775
5478
  ]));
4776
- return remote(name, { ...def, variants });
5479
+ return remote(name, { ...def, host: hostNamed(GATEWAY_HOST), variants });
4777
5480
  }
4778
- 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;
5481
+ var GATEWAY_HOST = "neko", 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, QueuedInputSchema, WithdrawResultSchema, SteerResultSchema, ChatViewSchema, SessionSummarySchema, DirectoryStateSchema, WorkspaceSummarySchema, WorkspaceStateSchema, credentialStatus, CredentialStateSchema, AttachmentSchema, chatInput, NekoSessionDirectory, NekoSessionManagement, resize, inputLimits, modelInfo, NekoModels, imageChunk, NekoSubagent, NekoChat, browse, page, NekoWorkspaces, provider, NekoCredentials, SearchSettingsStateSchema, NekoSearchSettings, contextSource, ContextSettingsStateSchema, nullable = (schema) => Type5.Union([schema, nil]), ContextSettingsPatchSchema, NekoContextSettings, FileCandidatesSchema, NekoWorkspaceFiles, marketSkill, preview, scope, installResult, NekoSkillMarket, ApplicationInfoSchema, NekoApplicationInfo;
4779
5482
  var init_contract = __esm(() => {
4780
5483
  str = Type5.String();
4781
5484
  num = Type5.Number();
@@ -4871,10 +5574,29 @@ var init_contract = __esm(() => {
4871
5574
  truncated: bool,
4872
5575
  dropped: integer
4873
5576
  });
5577
+ QueuedInputSchema = Type5.Object({
5578
+ id: integer,
5579
+ mode: Type5.Enum(["steer", "followUp"]),
5580
+ preview: str,
5581
+ images: integer,
5582
+ skill: Type5.Optional(Type5.Object({ name: str, args: str }))
5583
+ });
5584
+ WithdrawResultSchema = Type5.Object({
5585
+ result: Type5.Enum(["withdrawn", "placed", "settled", "unknown"]),
5586
+ text: Type5.Optional(str),
5587
+ images: Type5.Optional(integer)
5588
+ });
5589
+ SteerResultSchema = Type5.Object({
5590
+ result: Type5.Enum(["steered", "withdrawn", "placed", "settled", "unknown"]),
5591
+ id: Type5.Optional(integer),
5592
+ text: Type5.Optional(str),
5593
+ images: Type5.Optional(integer)
5594
+ });
4874
5595
  ChatViewSchema = Type5.Object({
4875
5596
  historyBefore: Type5.Optional(Type5.Union([integer, nil])),
4876
5597
  agents: Type5.Optional(Type5.Array(SubagentSummarySchema)),
4877
5598
  jobs: Type5.Optional(Type5.Array(JobSummarySchema)),
5599
+ queued: Type5.Optional(Type5.Array(QueuedInputSchema)),
4878
5600
  rounds: Type5.Optional(Type5.Array(DelegationSchema)),
4879
5601
  subagents: Type5.Optional(Type5.Array(DelegationSchema)),
4880
5602
  taskUsage: Type5.Optional(usage),
@@ -5079,6 +5801,8 @@ var init_contract = __esm(() => {
5079
5801
  whenBusy: Type5.Optional(Type5.Enum(["steer", "followUp"]))
5080
5802
  }, { additionalProperties: false }), nil),
5081
5803
  abort: rpc(nil, nil),
5804
+ withdraw: rpc(Type5.Object({ id: integer }, { additionalProperties: false }), WithdrawResultSchema),
5805
+ steer: rpc(Type5.Object({ id: integer }, { additionalProperties: false }), SteerResultSchema),
5082
5806
  usage: rpc(nil, usage),
5083
5807
  skills: rpc(nil, Type5.Array(Type5.Object({ name: str, description: str, location: str, level }))),
5084
5808
  jobOutput: rpc(Type5.Object({ jobId: str, since: Type5.Optional(integer) }, { additionalProperties: false }), JobOutputSchema),
@@ -5130,6 +5854,77 @@ var init_contract = __esm(() => {
5130
5854
  }
5131
5855
  }
5132
5856
  });
5857
+ SearchSettingsStateSchema = Type5.Object({
5858
+ revision: integer,
5859
+ selected: Type5.Enum(["tavily", "deepseek"])
5860
+ });
5861
+ NekoSearchSettings = contract("neko.search-settings", {
5862
+ version: 1,
5863
+ input: Type5.Object({}, { additionalProperties: false }),
5864
+ variants: {
5865
+ ready: {
5866
+ state: { state: SearchSettingsStateSchema },
5867
+ methods: {
5868
+ set: rpc(Type5.Object({ provider: Type5.Enum(["tavily", "deepseek"]) }, { additionalProperties: false }), nil)
5869
+ }
5870
+ }
5871
+ }
5872
+ });
5873
+ contextSource = Type5.Enum(["default", "config", "saved"]);
5874
+ ContextSettingsStateSchema = Type5.Object({
5875
+ revision: integer,
5876
+ effective: Type5.Object({
5877
+ compaction: Type5.Object({
5878
+ enabled: bool,
5879
+ triggerPercent: Type5.Union([integer, nil]),
5880
+ keepRecentTokens: integer
5881
+ }),
5882
+ pruning: Type5.Object({
5883
+ enabled: bool,
5884
+ triggerPercent: integer,
5885
+ keepRecent: integer,
5886
+ minChars: integer,
5887
+ minSavePercent: integer
5888
+ })
5889
+ }),
5890
+ sources: Type5.Object({
5891
+ compaction: Type5.Object({ enabled: contextSource, triggerPercent: contextSource, keepRecentTokens: contextSource }),
5892
+ pruning: Type5.Object({
5893
+ enabled: contextSource,
5894
+ triggerPercent: contextSource,
5895
+ keepRecent: contextSource,
5896
+ minChars: contextSource,
5897
+ minSavePercent: contextSource
5898
+ })
5899
+ })
5900
+ });
5901
+ ContextSettingsPatchSchema = Type5.Object({
5902
+ compaction: Type5.Optional(Type5.Object({
5903
+ enabled: Type5.Optional(nullable(bool)),
5904
+ triggerPercent: Type5.Optional(nullable(integer)),
5905
+ keepRecentTokens: Type5.Optional(nullable(integer))
5906
+ }, { additionalProperties: false })),
5907
+ pruning: Type5.Optional(Type5.Object({
5908
+ enabled: Type5.Optional(nullable(bool)),
5909
+ triggerPercent: Type5.Optional(nullable(integer)),
5910
+ keepRecent: Type5.Optional(nullable(integer)),
5911
+ minChars: Type5.Optional(nullable(integer)),
5912
+ minSavePercent: Type5.Optional(nullable(integer))
5913
+ }, { additionalProperties: false }))
5914
+ }, { additionalProperties: false });
5915
+ NekoContextSettings = contract("neko.context-settings", {
5916
+ version: 1,
5917
+ input: Type5.Object({}, { additionalProperties: false }),
5918
+ variants: {
5919
+ ready: {
5920
+ state: { state: ContextSettingsStateSchema },
5921
+ methods: {
5922
+ set: rpc(ContextSettingsPatchSchema, nil),
5923
+ reset: rpc(Type5.Object({}, { additionalProperties: false }), nil)
5924
+ }
5925
+ }
5926
+ }
5927
+ });
5133
5928
  FileCandidatesSchema = Type5.Object({
5134
5929
  entries: Type5.Array(Type5.Object({ name: str, path: str, kind: Type5.Enum(["file", "directory"]) }), { maxItems: 100 }),
5135
5930
  limited: bool,
@@ -5183,18 +5978,24 @@ var init_contract = __esm(() => {
5183
5978
  protocolVersion: Type5.Integer({ minimum: 1 }),
5184
5979
  latestVersion: nullableString,
5185
5980
  checkedAt: Type5.Union([integer, nil]),
5186
- checkState: Type5.Enum(["unknown", "checking", "ok", "failed", "development"])
5981
+ checkState: Type5.Enum(["unknown", "checking", "ok", "failed", "development"]),
5982
+ serverId: str
5187
5983
  });
5188
5984
  NekoApplicationInfo = contract("neko.application-info", {
5189
5985
  version: 1,
5190
5986
  input: Type5.Object({}),
5191
- variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
5987
+ variants: {
5988
+ ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } }
5989
+ }
5192
5990
  });
5193
5991
  });
5194
5992
 
5195
5993
  // packages/gateway/src/link.ts
5196
- import { webSocketLink } from "@earendil-works/chord";
5197
- function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
5994
+ import {
5995
+ createHost,
5996
+ webSocketLink
5997
+ } from "@earendil-works/chord";
5998
+ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES, options) {
5198
5999
  let queued = 0, incoming = 0, receiving = false;
5199
6000
  let native;
5200
6001
  const bridge = {
@@ -5210,18 +6011,22 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
5210
6011
  },
5211
6012
  close: (code, reason) => socket.close(code, reason),
5212
6013
  addEventListener(type, handler) {
5213
- if (type === "message")
5214
- socket.addEventListener("message", (event) => {
5215
- const bytes = size(String(event.data));
6014
+ if (type === "message") {
6015
+ const deliver = (data) => {
6016
+ const bytes = size(data);
5216
6017
  if (!receiving)
5217
6018
  incoming += bytes;
5218
6019
  if (bytes > max || incoming > max) {
5219
6020
  native.close("frame limit exceeded");
5220
6021
  return;
5221
6022
  }
5222
- handler(event);
5223
- });
5224
- else
6023
+ handler({ data });
6024
+ };
6025
+ if (socket.on)
6026
+ socket.on("message", (data, isBinary) => deliver(isBinary ? asBytes(data) : new TextDecoder().decode(asBytes(data))));
6027
+ else
6028
+ socket.addEventListener("message", (event) => deliver(typeof event.data === "string" || event.data instanceof Uint8Array ? event.data : String(event.data)));
6029
+ } else
5225
6030
  socket.addEventListener(type, () => {
5226
6031
  if (type === "open")
5227
6032
  queued = 0;
@@ -5233,7 +6038,7 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
5233
6038
  });
5234
6039
  }
5235
6040
  };
5236
- native = webSocketLink(bridge);
6041
+ native = webSocketLink(bridge, options);
5237
6042
  socket.addEventListener("error", () => native.close("socket error"));
5238
6043
  return {
5239
6044
  get closed() {
@@ -5253,7 +6058,7 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
5253
6058
  }
5254
6059
  if (socket.readyState === 0)
5255
6060
  queued += bytes;
5256
- native.send(frame);
6061
+ return native.send(frame);
5257
6062
  },
5258
6063
  receive(handler) {
5259
6064
  receiving = true;
@@ -5264,49 +6069,19 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
5264
6069
  close: (reason) => native.close(reason)
5265
6070
  };
5266
6071
  }
5267
- function identifiedLink(link, expectedName) {
5268
- let receiver;
5269
- let seen = false, pendingBytes = 0;
5270
- const pending = [];
5271
- link.receive((frame) => {
5272
- if (!seen) {
5273
- if (frame.t !== "hello" || frame.name !== expectedName) {
5274
- link.close("serverId mismatch");
5275
- return;
5276
- }
5277
- seen = true;
5278
- }
5279
- if (receiver)
5280
- receiver(frame);
5281
- else {
5282
- pendingBytes += size(JSON.stringify(frame));
5283
- if (pendingBytes > MAX_LINK_BYTES) {
5284
- link.close("receive queue limit exceeded");
5285
- pending.length = 0;
5286
- return;
5287
- }
5288
- pending.push(frame);
5289
- }
5290
- });
5291
- return {
5292
- get closed() {
5293
- return link.closed;
5294
- },
5295
- send: (frame) => link.send(frame),
5296
- receive(handler) {
5297
- receiver = handler;
5298
- pendingBytes = 0;
5299
- for (const frame of pending.splice(0)) {
5300
- if (link.closed)
5301
- break;
5302
- handler(frame);
5303
- }
6072
+ function createClientHost(link, options = {}) {
6073
+ return createHost({
6074
+ kind: "client",
6075
+ self: { kind: "client" },
6076
+ upstream: {
6077
+ link,
6078
+ principal: { kind: "server" },
6079
+ ...options.onProtocolMismatch ? { onProtocolMismatch: options.onProtocolMismatch } : {}
5304
6080
  },
5305
- onClose: (handler) => link.onClose(handler),
5306
- close: (reason) => link.close(reason)
5307
- };
6081
+ onError: () => {}
6082
+ });
5308
6083
  }
5309
- var MAX_LINK_BYTES, size = (text) => new TextEncoder().encode(text).byteLength;
6084
+ var MAX_LINK_BYTES, asBytes = (data) => data instanceof Uint8Array ? data : new Uint8Array(data), size = (data) => typeof data === "string" ? new TextEncoder().encode(data).byteLength : data.byteLength;
5310
6085
  var init_link = __esm(() => {
5311
6086
  MAX_LINK_BYTES = 16 * 1024 * 1024;
5312
6087
  });
@@ -5494,35 +6269,14 @@ class ApprovalQueue {
5494
6269
  }
5495
6270
 
5496
6271
  // packages/gateway/src/client-session.ts
5497
- import { createHost } from "@earendil-works/chord";
5498
- function waitForValue(state, signal) {
5499
- return new Promise((resolve, reject) => {
5500
- let stop = () => {};
5501
- let settled = false;
5502
- const finish = (error, value) => {
5503
- if (settled)
5504
- return;
5505
- settled = true;
5506
- clearTimeout(timer);
5507
- stop();
5508
- signal?.removeEventListener("abort", abort);
5509
- if (error)
5510
- reject(error);
5511
- else
5512
- resolve(value);
5513
- };
5514
- const abort = () => finish(signal?.reason ?? new Error("Cancelled"));
5515
- const timer = setTimeout(() => finish(new Error("Timed out waiting for first snapshot")), 5000);
5516
- stop = state.subscribe((value) => {
5517
- if (value !== undefined)
5518
- finish(undefined, value);
5519
- });
5520
- if (settled)
5521
- stop();
5522
- signal?.addEventListener("abort", abort, { once: true });
5523
- if (signal?.aborted)
5524
- abort();
5525
- });
6272
+ async function assertGatewayIdentity(host, serverId, signal) {
6273
+ const application = await host.get(NekoApplicationInfo, {}, signal ? { signal } : {});
6274
+ try {
6275
+ if (application.info.value.serverId !== serverId)
6276
+ throw new GatewayIdentityError;
6277
+ } finally {
6278
+ application.release();
6279
+ }
5526
6280
  }
5527
6281
  function jobsClient(chat) {
5528
6282
  const supported = () => chat.view.value?.jobs !== undefined;
@@ -5545,17 +6299,57 @@ function jobsClient(chat) {
5545
6299
  }
5546
6300
  };
5547
6301
  }
6302
+ function queueClient(chat) {
6303
+ const supported = () => chat.view.value?.queued !== undefined;
6304
+ return {
6305
+ get supported() {
6306
+ return supported();
6307
+ },
6308
+ list: () => chat.view.value?.queued ?? [],
6309
+ async steer(id) {
6310
+ if (!supported())
6311
+ throw new Error(QUEUE_UNSUPPORTED);
6312
+ return chat.steer({ id });
6313
+ },
6314
+ async withdraw(id) {
6315
+ if (!supported())
6316
+ throw new Error(QUEUE_UNSUPPORTED);
6317
+ return chat.withdraw({ id });
6318
+ }
6319
+ };
6320
+ }
5548
6321
  function createGatewayConnection(link, serverId) {
5549
- const host = createHost({ kind: "client", upstream: identifiedLink(link, serverId), onError: () => {} });
5550
- const get = (type) => host.get(type, {}, { at: serverId });
6322
+ let protocolMismatch = false;
6323
+ const linkClosed = new AbortController;
6324
+ let closeReason = "";
6325
+ link.onClose((reason) => {
6326
+ closeReason = reason;
6327
+ linkClosed.abort(new Error(`Gateway connection closed: ${reason}`));
6328
+ });
6329
+ const host = createClientHost(link, {
6330
+ onProtocolMismatch: () => {
6331
+ protocolMismatch = true;
6332
+ }
6333
+ });
6334
+ const get = (type) => host.hold(type, {});
5551
6335
  const application = get(NekoApplicationInfo);
5552
6336
  const keep = [application.info.subscribe(() => {})];
5553
6337
  let business;
5554
6338
  let protocolChecked = false;
5555
6339
  function bindBusiness() {
5556
- const directory = get(NekoSessionDirectory), management = get(NekoSessionManagement), models = get(NekoModels), workspaces = get(NekoWorkspaces), workspaceFiles = get(NekoWorkspaceFiles), credentials = get(NekoCredentials), skillMarket = get(NekoSkillMarket);
5557
- keep.push(directory.state.subscribe(() => {}), workspaces.state.subscribe(() => {}), credentials.state.subscribe(() => {}));
5558
- return { directory, management, models, workspaces, workspaceFiles, credentials, skillMarket };
6340
+ const directory = get(NekoSessionDirectory), management = get(NekoSessionManagement), models = get(NekoModels), workspaces = get(NekoWorkspaces), workspaceFiles = get(NekoWorkspaceFiles), credentials = get(NekoCredentials), searchSettings = get(NekoSearchSettings), contextSettings = get(NekoContextSettings), skillMarket = get(NekoSkillMarket);
6341
+ keep.push(directory.state.subscribe(() => {}), workspaces.state.subscribe(() => {}), credentials.state.subscribe(() => {}), searchSettings.state.subscribe(() => {}), contextSettings.state.subscribe(() => {}));
6342
+ return {
6343
+ directory,
6344
+ management,
6345
+ models,
6346
+ workspaces,
6347
+ workspaceFiles,
6348
+ credentials,
6349
+ searchSettings,
6350
+ contextSettings,
6351
+ skillMarket
6352
+ };
5559
6353
  }
5560
6354
  const services = () => {
5561
6355
  if (!protocolChecked)
@@ -5564,6 +6358,37 @@ function createGatewayConnection(link, serverId) {
5564
6358
  business = bindBusiness();
5565
6359
  return business;
5566
6360
  };
6361
+ const checkReady = async (signal) => {
6362
+ await deadline(application.ready, signal);
6363
+ const info = application.info.value;
6364
+ if (info.serverId !== serverId)
6365
+ throw new GatewayIdentityError;
6366
+ if (info.protocolVersion !== APPLICATION_PROTOCOL)
6367
+ throw new ApplicationProtocolError;
6368
+ protocolChecked = true;
6369
+ const {
6370
+ directory,
6371
+ management,
6372
+ models,
6373
+ workspaces,
6374
+ credentials,
6375
+ searchSettings,
6376
+ contextSettings,
6377
+ workspaceFiles,
6378
+ skillMarket
6379
+ } = services();
6380
+ await deadline(Promise.all([
6381
+ directory.ready,
6382
+ management.ready,
6383
+ models.ready,
6384
+ workspaces.ready,
6385
+ credentials.ready,
6386
+ searchSettings.ready,
6387
+ contextSettings.ready,
6388
+ workspaceFiles.ready,
6389
+ skillMarket.ready
6390
+ ]), signal);
6391
+ };
5567
6392
  let chat;
5568
6393
  let attachment;
5569
6394
  let stopView;
@@ -5575,7 +6400,7 @@ function createGatewayConnection(link, serverId) {
5575
6400
  if (!attachment)
5576
6401
  throw new Error("No active session");
5577
6402
  const input = { ...attachment, ...typeof target === "string" ? { agentId: target } : { taskId: target } };
5578
- return subagentClient(() => host.get(NekoSubagent, input, { at: serverId }));
6403
+ return subagentClient(() => host.hold(NekoSubagent, input));
5579
6404
  },
5580
6405
  host,
5581
6406
  link,
@@ -5597,6 +6422,12 @@ function createGatewayConnection(link, serverId) {
5597
6422
  get credentials() {
5598
6423
  return services().credentials;
5599
6424
  },
6425
+ get searchSettings() {
6426
+ return services().searchSettings;
6427
+ },
6428
+ get contextSettings() {
6429
+ return services().contextSettings;
6430
+ },
5600
6431
  get skillMarket() {
5601
6432
  return services().skillMarket;
5602
6433
  },
@@ -5604,26 +6435,13 @@ function createGatewayConnection(link, serverId) {
5604
6435
  return attachment;
5605
6436
  },
5606
6437
  async ready(signal) {
5607
- await deadline(application.ready, signal);
5608
- const info = await waitForValue(application.info, signal);
5609
- if (info.protocolVersion !== APPLICATION_PROTOCOL)
5610
- throw new ApplicationProtocolError;
5611
- protocolChecked = true;
5612
- const { directory, management, models, workspaces, credentials, workspaceFiles, skillMarket } = services();
5613
- await deadline(Promise.all([
5614
- directory.ready,
5615
- management.ready,
5616
- models.ready,
5617
- workspaces.ready,
5618
- credentials.ready,
5619
- workspaceFiles.ready,
5620
- skillMarket.ready
5621
- ]), signal);
5622
- await Promise.all([
5623
- waitForValue(directory.state, signal),
5624
- waitForValue(workspaces.state, signal),
5625
- waitForValue(credentials.state, signal)
5626
- ]);
6438
+ try {
6439
+ await checkReady(signal ? AbortSignal.any([signal, linkClosed.signal]) : linkClosed.signal);
6440
+ } catch (error) {
6441
+ if (protocolMismatch || closeReason.startsWith("protocol"))
6442
+ throw new ApplicationProtocolError;
6443
+ throw error;
6444
+ }
5627
6445
  },
5628
6446
  async attach(sessionId, ctx) {
5629
6447
  const next = await connection.management.attach(sessionId, ctx);
@@ -5632,12 +6450,11 @@ function createGatewayConnection(link, serverId) {
5632
6450
  stopView?.();
5633
6451
  chat?.release();
5634
6452
  attachment = next;
5635
- const current = host.get(NekoChat, next, { at: serverId });
6453
+ const current = host.hold(NekoChat, next);
5636
6454
  chat = current;
5637
6455
  stopView = current.view.subscribe(() => {});
5638
6456
  try {
5639
6457
  await deadline(current.ready, ctx?.signal);
5640
- await waitForValue(current.view, ctx?.signal);
5641
6458
  if (current !== chat)
5642
6459
  throw new Error("Attachment changed");
5643
6460
  return current;
@@ -5697,11 +6514,17 @@ function deadline(work, signal) {
5697
6514
  signal?.removeEventListener("abort", abort);
5698
6515
  });
5699
6516
  }
5700
- var JOBS_UNSUPPORTED = "This gateway does not support background jobs. Restart the gateway to enable them.";
6517
+ var GatewayIdentityError, JOBS_UNSUPPORTED = "This gateway does not support background jobs. Restart the gateway to enable them.", QUEUE_UNSUPPORTED = "This gateway does not support queued message controls. Restart the gateway to enable them.";
5701
6518
  var init_client_session = __esm(() => {
5702
6519
  init_application_version();
5703
6520
  init_contract();
5704
6521
  init_link();
6522
+ GatewayIdentityError = class GatewayIdentityError extends Error {
6523
+ constructor() {
6524
+ super("Gateway identity mismatch");
6525
+ this.name = "GatewayIdentityError";
6526
+ }
6527
+ };
5705
6528
  });
5706
6529
 
5707
6530
  // packages/tui/src/theme/palette.ts
@@ -17881,6 +18704,8 @@ var init_labels = __esm(() => {
17881
18704
  { name: "model", description: "Choose a model" },
17882
18705
  { name: "agents", description: "View agent tasks and activity" },
17883
18706
  { name: "jobs", description: "View background jobs" },
18707
+ { name: "search", description: "Choose the web search provider (Tavily/DeepSeek)" },
18708
+ { name: "context", description: "Tune context compaction and tool result pruning" },
17884
18709
  { name: "theme", description: "Choose color theme (auto/light/dark)" },
17885
18710
  { name: "think", description: "Set thinking level (/think high)" },
17886
18711
  { name: "fork", description: "Fork this session" },
@@ -17903,9 +18728,9 @@ var init_labels = __esm(() => {
17903
18728
  return shortcutHints(["\u2191\u2193", "Select"], ["Enter", "Open"], ["Esc", "Close"]);
17904
18729
  },
17905
18730
  idleHints: "Enter Send \xB7 /help Keys",
17906
- busyHints: "Enter Steer \xB7 Esc Stop",
18731
+ busyHints: "Enter Queue \xB7 Esc Stop",
17907
18732
  idleCompact: "Enter Send \xB7 /help Keys",
17908
- busyCompact: "Enter Steer \xB7 Esc Stop"
18733
+ busyCompact: "Enter Queue \xB7 Esc Stop"
17909
18734
  };
17910
18735
  });
17911
18736
 
@@ -19390,6 +20215,227 @@ var init_welcome = __esm(() => {
19390
20215
  CAT = ["|\\___/|", ") (", "=\\^_^ /=", ")---("];
19391
20216
  });
19392
20217
 
20218
+ // packages/tui/src/context-ui.ts
20219
+ import { matchesKey as matchesKey7 } from "@earendil-works/pi-tui";
20220
+ function format(kind, value) {
20221
+ if (value === null)
20222
+ return "Default";
20223
+ if (typeof value === "boolean")
20224
+ return value ? "On" : "Off";
20225
+ switch (kind) {
20226
+ case "percent":
20227
+ return `${value}%`;
20228
+ case "tokens":
20229
+ return value >= 1000 ? `${Math.round(value / 1000)}k tokens` : `${value} tokens`;
20230
+ case "chars":
20231
+ return value >= 1000 ? `${Math.round(value / 1000)}k chars` : `${value} chars`;
20232
+ default:
20233
+ return String(value);
20234
+ }
20235
+ }
20236
+ function read(state, row) {
20237
+ const effective = state.effective[row.group];
20238
+ const sources = state.sources[row.group];
20239
+ return { value: effective[row.field] ?? null, source: sources[row.field] ?? "default" };
20240
+ }
20241
+ function contextItems(state) {
20242
+ if (!state)
20243
+ return [];
20244
+ const items = [];
20245
+ let group = "";
20246
+ for (const row of CONTEXT_ROWS) {
20247
+ if (row.group !== group) {
20248
+ group = row.group;
20249
+ items.push({
20250
+ value: `header:${group}`,
20251
+ label: group === "compaction" ? "Compaction" : "Tool result pruning",
20252
+ header: true
20253
+ });
20254
+ }
20255
+ const { value, source } = read(state, row);
20256
+ const custom = source !== "default" && value !== null && !row.presets.includes(value);
20257
+ items.push({
20258
+ value: row.key,
20259
+ label: row.label,
20260
+ rightLabel: `${format(row.kind, value)} ${SOURCES[source]}${custom ? " \xB7 Custom" : ""}`,
20261
+ description: row.description
20262
+ });
20263
+ }
20264
+ items.push({
20265
+ value: RESET_VALUE,
20266
+ label: "Reset to defaults",
20267
+ description: "Clear every saved value. Config file values still apply."
20268
+ });
20269
+ return items;
20270
+ }
20271
+
20272
+ class PresetDetail {
20273
+ help = shortcutHints(["\u2191\u2193", "Select"], ["Enter", "Save"], ["Esc", "Back"]);
20274
+ #index = 0;
20275
+ #error = "";
20276
+ #saving = false;
20277
+ #options;
20278
+ #row;
20279
+ #client;
20280
+ #panel;
20281
+ #notify;
20282
+ #changed;
20283
+ constructor(row, client, panel, notify, changed) {
20284
+ this.#row = row;
20285
+ this.#client = client;
20286
+ this.#panel = panel;
20287
+ this.#notify = notify;
20288
+ this.#changed = changed;
20289
+ const state = client.contextSettings.state.value;
20290
+ const current = state ? read(state, row) : { value: null, source: "default" };
20291
+ const options = [null, ...row.presets];
20292
+ const custom = current.source !== "default" && current.value !== null && !row.presets.includes(current.value);
20293
+ if (custom)
20294
+ options.push(current.value);
20295
+ this.#options = options;
20296
+ this.#index = current.source === "saved" || custom ? Math.max(0, options.indexOf(current.value)) : 0;
20297
+ }
20298
+ lines(_width, height) {
20299
+ const state = this.#client.contextSettings.state.value;
20300
+ const current = state ? read(state, this.#row) : undefined;
20301
+ const head = [theme.accent(this.#row.label), theme.dim(this.#row.description)];
20302
+ if (this.#saving)
20303
+ head.push("Saving...");
20304
+ if (this.#error)
20305
+ head.push(theme.error(`Error: ${this.#error}`));
20306
+ const options = this.#options.map((option, i) => {
20307
+ const custom = option !== null && !this.#row.presets.includes(option);
20308
+ const text = `${format(this.#row.kind, option)}${custom ? " (Custom)" : ""}${option === null && current ? ` - now ${SOURCES[current.source]}` : ""}`;
20309
+ return i === this.#index ? theme.selection(`\u276F ${text}`) : ` ${text}`;
20310
+ });
20311
+ const count = Math.max(1, height - head.length);
20312
+ const start = Math.max(0, Math.min(this.#index - Math.floor(count / 2), options.length - count));
20313
+ return [...head, ...options.slice(start, start + count)];
20314
+ }
20315
+ handleKey(data) {
20316
+ if (this.#saving)
20317
+ return true;
20318
+ if (matchesKey7(data, "up"))
20319
+ this.#index = Math.max(0, this.#index - 1);
20320
+ else if (matchesKey7(data, "down"))
20321
+ this.#index = Math.min(this.#options.length - 1, this.#index + 1);
20322
+ else if (matchesKey7(data, "enter")) {
20323
+ const value = this.#options[this.#index] ?? null;
20324
+ this.#saving = true;
20325
+ this.#error = "";
20326
+ const patch = { [this.#row.group]: { [this.#row.field]: value } };
20327
+ this.#client.contextSettings.set(patch).then(() => {
20328
+ this.#saving = false;
20329
+ this.#notify(`${this.#row.label} set to ${format(this.#row.kind, value)}.`);
20330
+ this.#panel.popDetail();
20331
+ }, (error) => {
20332
+ this.#saving = false;
20333
+ this.#error = message(error);
20334
+ this.#changed();
20335
+ });
20336
+ } else
20337
+ return false;
20338
+ return true;
20339
+ }
20340
+ }
20341
+ function openContextPanel(panel, client, notify) {
20342
+ const items = () => contextItems(client.contextSettings.state.value);
20343
+ const stop = client.contextSettings.state.subscribe(() => panel.update(items()));
20344
+ panel.emptyText = CONTEXT_EMPTY;
20345
+ panel.show("Context settings", async () => items(), async (value) => {
20346
+ if (value === RESET_VALUE) {
20347
+ await client.contextSettings.reset({});
20348
+ notify("Context settings reset to defaults.");
20349
+ return;
20350
+ }
20351
+ const row = CONTEXT_ROWS.find((item) => item.key === value);
20352
+ if (!row)
20353
+ return;
20354
+ return new PresetDetail(row, client, panel, notify, () => panel.changed());
20355
+ });
20356
+ return stop;
20357
+ }
20358
+ var CONTEXT_EMPTY = "Context settings are unavailable.", CONTEXT_ROWS, SOURCES, RESET_VALUE = "reset", message = (error) => error instanceof Error ? error.message : String(error);
20359
+ var init_context_ui = __esm(() => {
20360
+ init_shortcut_hints();
20361
+ init_theme();
20362
+ CONTEXT_ROWS = [
20363
+ {
20364
+ key: "compaction.enabled",
20365
+ group: "compaction",
20366
+ field: "enabled",
20367
+ label: "Auto compaction",
20368
+ kind: "bool",
20369
+ description: "Compact automatically near the limit. /compact always works.",
20370
+ presets: [true, false]
20371
+ },
20372
+ {
20373
+ key: "compaction.triggerPercent",
20374
+ group: "compaction",
20375
+ field: "triggerPercent",
20376
+ label: "Compact at",
20377
+ kind: "percent",
20378
+ description: "Compact when context reaches this share of the smallest model window. Default is near the limit.",
20379
+ presets: [30, 40, 50, 60, 70, 80]
20380
+ },
20381
+ {
20382
+ key: "compaction.keepRecentTokens",
20383
+ group: "compaction",
20384
+ field: "keepRecentTokens",
20385
+ label: "Keep recent",
20386
+ kind: "tokens",
20387
+ description: "Recent context a compaction keeps verbatim.",
20388
+ presets: [1e4, 20000, 40000, 80000]
20389
+ },
20390
+ {
20391
+ key: "pruning.enabled",
20392
+ group: "pruning",
20393
+ field: "enabled",
20394
+ label: "Prune tool results",
20395
+ kind: "bool",
20396
+ description: "Replace old, large tool results with a short note in what the model sees. History is kept.",
20397
+ presets: [true, false]
20398
+ },
20399
+ {
20400
+ key: "pruning.triggerPercent",
20401
+ group: "pruning",
20402
+ field: "triggerPercent",
20403
+ label: "Prune at",
20404
+ kind: "percent",
20405
+ description: "Prune when context reaches this share of the model window. Each prune invalidates the prompt cache.",
20406
+ presets: [20, 30, 35, 45, 55]
20407
+ },
20408
+ {
20409
+ key: "pruning.keepRecent",
20410
+ group: "pruning",
20411
+ field: "keepRecent",
20412
+ label: "Keep recent results",
20413
+ kind: "count",
20414
+ description: "Newest tool results that are never pruned.",
20415
+ presets: [5, 10, 20, 40]
20416
+ },
20417
+ {
20418
+ key: "pruning.minChars",
20419
+ group: "pruning",
20420
+ field: "minChars",
20421
+ label: "Min result size",
20422
+ kind: "chars",
20423
+ description: "Results shorter than this are never pruned.",
20424
+ presets: [2000, 4000, 8000, 16000]
20425
+ },
20426
+ {
20427
+ key: "pruning.minSavePercent",
20428
+ group: "pruning",
20429
+ field: "minSavePercent",
20430
+ label: "Min release",
20431
+ kind: "percent",
20432
+ description: "A prune runs only if it frees at least this share of the model window. Each prune invalidates the prompt cache, so a larger share means fewer prunes.",
20433
+ presets: [2, 5, 10, 15]
20434
+ }
20435
+ ];
20436
+ SOURCES = { default: "Default", config: "Config", saved: "Saved" };
20437
+ });
20438
+
19393
20439
  // packages/tui/src/detect-theme.ts
19394
20440
  function detectColorFgBgMode(env = process.env) {
19395
20441
  const bg = env.COLORFGBG?.split(";").at(-1)?.trim();
@@ -19695,7 +20741,7 @@ class UnixLink {
19695
20741
  }
19696
20742
  send(frame) {
19697
20743
  try {
19698
- this.#native.send(frame);
20744
+ return this.#native.send(frame);
19699
20745
  } catch (error) {
19700
20746
  this.#native.close("pending byte limit exceeded");
19701
20747
  throw error;
@@ -19829,7 +20875,7 @@ function createWsListener(options) {
19829
20875
  server.on("error", (error) => options.onError?.(error));
19830
20876
  server.on("connection", (socket) => {
19831
20877
  socket.on("error", (error) => options.onError?.(error));
19832
- accept(boundedWebSocketLink(socket, options.maxPendingBytes));
20878
+ accept(boundedWebSocketLink(socket, options.maxPendingBytes, { acceptPacked: false }));
19833
20879
  });
19834
20880
  },
19835
20881
  async close() {
@@ -19879,6 +20925,25 @@ function isServerId(value) {
19879
20925
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
19880
20926
  }
19881
20927
 
20928
+ // packages/gateway/src/principal.ts
20929
+ function gatewayPrincipalKey(principal) {
20930
+ switch (principal.kind) {
20931
+ case "mobile":
20932
+ return `mobile:${principal.deviceId}:${principal.grantRevision}:${principal.connectionId}`;
20933
+ case "local":
20934
+ return `local:${principal.connectionId}`;
20935
+ case "host":
20936
+ return "host";
20937
+ }
20938
+ }
20939
+ function isMobilePrincipal(principal) {
20940
+ return typeof principal === "object" && principal !== null && principal.kind === "mobile";
20941
+ }
20942
+ var HOST_PRINCIPAL;
20943
+ var init_principal = __esm(() => {
20944
+ HOST_PRINCIPAL = { kind: "host" };
20945
+ });
20946
+
19882
20947
  // packages/gateway/src/mobile/store.ts
19883
20948
  import { randomBytes } from "crypto";
19884
20949
  import { chmod as chmod2, mkdir as mkdir5, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
@@ -20189,20 +21254,8 @@ var init_store = __esm(() => {
20189
21254
 
20190
21255
  // packages/gateway/src/mobile/admin.ts
20191
21256
  import { createHash, timingSafeEqual } from "crypto";
20192
- import { ChordError, method as method2, remote as remote2 } from "@earendil-works/chord";
21257
+ import { ChordError } from "@earendil-works/chord";
20193
21258
  import { Type as Type6 } from "typebox";
20194
- function contract2(name, def) {
20195
- const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
20196
- key,
20197
- {
20198
- methods: Object.fromEntries(Object.entries(value.methods ?? {}).map(([member, [args, result]]) => [member, method2(args, result)]))
20199
- }
20200
- ]));
20201
- return remote2(name, { ...def, variants });
20202
- }
20203
- function isMobilePrincipal(principal) {
20204
- return typeof principal === "object" && principal !== null && principal.kind === "mobile";
20205
- }
20206
21259
  function digest(value) {
20207
21260
  return createHash("sha256").update(value, "utf8").digest();
20208
21261
  }
@@ -20231,7 +21284,7 @@ function createMobileAdminFactory(deps) {
20231
21284
  throw new ChordError("forbidden", "forbidden");
20232
21285
  };
20233
21286
  return {
20234
- kind: "admin",
21287
+ variant: "admin",
20235
21288
  async state() {
20236
21289
  await guard();
20237
21290
  return deps.mobile.adminState();
@@ -20269,6 +21322,8 @@ function createMobileAdminFactory(deps) {
20269
21322
  }
20270
21323
  var listenerStateSchema, adminDeviceSchema, NekoMobileAdmin;
20271
21324
  var init_admin = __esm(() => {
21325
+ init_contract();
21326
+ init_principal();
20272
21327
  init_store();
20273
21328
  listenerStateSchema = Type6.Union([
20274
21329
  Type6.Object({ status: Type6.Literal("disabled") }),
@@ -20290,54 +21345,42 @@ var init_admin = __esm(() => {
20290
21345
  lastSeenAt: Type6.Integer(),
20291
21346
  online: Type6.Boolean()
20292
21347
  });
20293
- NekoMobileAdmin = contract2("neko.mobile-admin", {
21348
+ NekoMobileAdmin = contract("neko.mobile-admin", {
20294
21349
  version: 1,
20295
21350
  input: Type6.Object({ token: Type6.String() }),
20296
21351
  variants: {
20297
21352
  admin: {
20298
21353
  methods: {
20299
- state: [
20300
- Type6.Null(),
20301
- Type6.Object({
20302
- state: listenerStateSchema,
20303
- candidates: Type6.Array(Type6.String()),
20304
- port: Type6.Integer({ minimum: 1, maximum: 65535 }),
20305
- certificate: Type6.Union([
20306
- Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() }),
20307
- Type6.Null()
20308
- ]),
20309
- devices: Type6.Array(adminDeviceSchema),
20310
- lastConnectedAddress: Type6.Union([Type6.String(), Type6.Null()]),
20311
- wsl: Type6.Optional(Type6.Union([
20312
- Type6.Object({ interop: Type6.Boolean(), error: Type6.Union([Type6.String(), Type6.Null()]) }),
20313
- Type6.Null()
20314
- ]))
20315
- })
20316
- ],
20317
- enable: [
20318
- Type6.Object({
20319
- address: Type6.String(),
20320
- port: Type6.Optional(Type6.Integer({ minimum: 1, maximum: 65535 }))
20321
- }),
20322
- Type6.Object({
20323
- status: Type6.Literal("listening"),
20324
- address: Type6.String(),
20325
- port: Type6.Integer({ minimum: 1, maximum: 65535 }),
20326
- bindAddress: Type6.Optional(Type6.String()),
20327
- mode: Type6.Optional(Type6.Union([Type6.Literal("direct"), Type6.Literal("wsl-relay")]))
20328
- })
20329
- ],
20330
- disable: [Type6.Null(), listenerStateSchema],
20331
- createPairing: [Type6.Null(), Type6.Object({ pairingText: Type6.String(), expiresAt: Type6.Integer() })],
20332
- cancelPairing: [Type6.Null(), Type6.Null()],
20333
- revoke: [
20334
- Type6.Object({ deviceId: Type6.String({ minLength: 1 }) }),
20335
- Type6.Object({ revoked: Type6.Boolean() })
20336
- ],
20337
- regenerateCertificate: [
20338
- Type6.Null(),
20339
- Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() })
20340
- ]
21354
+ state: rpc(Type6.Null(), Type6.Object({
21355
+ state: listenerStateSchema,
21356
+ candidates: Type6.Array(Type6.String()),
21357
+ port: Type6.Integer({ minimum: 1, maximum: 65535 }),
21358
+ certificate: Type6.Union([
21359
+ Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() }),
21360
+ Type6.Null()
21361
+ ]),
21362
+ devices: Type6.Array(adminDeviceSchema),
21363
+ lastConnectedAddress: Type6.Union([Type6.String(), Type6.Null()]),
21364
+ wsl: Type6.Optional(Type6.Union([
21365
+ Type6.Object({ interop: Type6.Boolean(), error: Type6.Union([Type6.String(), Type6.Null()]) }),
21366
+ Type6.Null()
21367
+ ]))
21368
+ })),
21369
+ enable: rpc(Type6.Object({
21370
+ address: Type6.String(),
21371
+ port: Type6.Optional(Type6.Integer({ minimum: 1, maximum: 65535 }))
21372
+ }), Type6.Object({
21373
+ status: Type6.Literal("listening"),
21374
+ address: Type6.String(),
21375
+ port: Type6.Integer({ minimum: 1, maximum: 65535 }),
21376
+ bindAddress: Type6.Optional(Type6.String()),
21377
+ mode: Type6.Optional(Type6.Union([Type6.Literal("direct"), Type6.Literal("wsl-relay")]))
21378
+ })),
21379
+ disable: rpc(Type6.Null(), listenerStateSchema),
21380
+ createPairing: rpc(Type6.Null(), Type6.Object({ pairingText: Type6.String(), expiresAt: Type6.Integer() })),
21381
+ cancelPairing: rpc(Type6.Null(), Type6.Null()),
21382
+ revoke: rpc(Type6.Object({ deviceId: Type6.String({ minLength: 1 }) }), Type6.Object({ revoked: Type6.Boolean() })),
21383
+ regenerateCertificate: rpc(Type6.Null(), Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() }))
20341
21384
  }
20342
21385
  }
20343
21386
  }
@@ -20401,7 +21444,7 @@ function buildHelloResultSchema() {
20401
21444
  })
20402
21445
  });
20403
21446
  }
20404
- var MOBILE_LIMITS, MOBILE_API_VERSIONS, MOBILE_CAPABILITIES, MOBILE_CLOSE_REVOKED = 4401, MOBILE_CLOSE_INCOMPATIBLE = 4402, MOBILE_CLOSE_SLOW_CONSUMER = 4408, MobileHelloParams, opt, str2, int, nullable = (schema) => Type7.Union([schema, Type7.Null()]), MobileContentRefSchema, MobileImageRefSchema, MobileToolItemSchema, MobileChatItemSchema, MobileStreamingSchema, MobileUsageSchema, MobileSubagentRoundSchema, MobileAgentSchema, MobileChatStatusSchema, MobileChatSnapshotSchema, MobileWorkspaceSchema, MobileSessionSummarySchema, MobileApprovalSchema, sessionListParams, sessionListResult, subscription, pageResult, pageParams, chunkResult, MOBILE_API_V1_METHODS, patchBody, MOBILE_API_V1_NOTIFICATIONS;
21447
+ var MOBILE_LIMITS, MOBILE_API_VERSIONS, MOBILE_CAPABILITIES, MOBILE_CLOSE_REVOKED = 4401, MOBILE_CLOSE_INCOMPATIBLE = 4402, MOBILE_CLOSE_SLOW_CONSUMER = 4408, MobileHelloParams, opt, str2, int, nullable2 = (schema) => Type7.Union([schema, Type7.Null()]), MobileContentRefSchema, MobileImageRefSchema, MobileToolItemSchema, MobileChatItemSchema, MobileStreamingSchema, MobileUsageSchema, MobileSubagentRoundSchema, MobileAgentSchema, MobileQueuedSchema, MobileChatStatusSchema, MobileChatSnapshotSchema, MobileWorkspaceSchema, MobileSessionSummarySchema, MobileApprovalSchema, sessionListParams, sessionListResult, subscription, pageResult, pageParams, chunkResult, MOBILE_API_V1_METHODS, patchBody, MOBILE_API_V1_NOTIFICATIONS;
20405
21448
  var init_protocol = __esm(() => {
20406
21449
  init_application_version();
20407
21450
  MOBILE_LIMITS = {
@@ -20520,7 +21563,7 @@ var init_protocol = __esm(() => {
20520
21563
  approvalCount: int,
20521
21564
  readonly: Type7.Boolean(),
20522
21565
  createdAt: Type7.Number(),
20523
- endedAt: nullable(Type7.Number()),
21566
+ endedAt: nullable2(Type7.Number()),
20524
21567
  usage: opt(MobileUsageSchema)
20525
21568
  });
20526
21569
  MobileAgentSchema = Type7.Object({
@@ -20528,18 +21571,25 @@ var init_protocol = __esm(() => {
20528
21571
  role: str2,
20529
21572
  title: str2,
20530
21573
  status: Type7.Union([Type7.Literal("idle"), Type7.Literal("queued"), Type7.Literal("running")]),
20531
- activeRoundId: nullable(int),
21574
+ activeRoundId: nullable2(int),
20532
21575
  lastRoundId: int,
20533
21576
  roundCount: int,
20534
21577
  approvalCount: int
20535
21578
  });
21579
+ MobileQueuedSchema = Type7.Object({
21580
+ id: int,
21581
+ mode: Type7.Union([Type7.Literal("steer"), Type7.Literal("followUp")]),
21582
+ preview: Type7.String(),
21583
+ images: int
21584
+ });
20536
21585
  MobileChatStatusSchema = Type7.Object({
20537
21586
  busy: Type7.Boolean(),
20538
- status: nullable(str2),
21587
+ queued: opt(Type7.Array(MobileQueuedSchema)),
21588
+ status: nullable2(str2),
20539
21589
  usage: MobileUsageSchema,
20540
21590
  taskUsage: opt(MobileUsageSchema),
20541
- contextUsage: opt(Type7.Object({ tokens: nullable(Type7.Number()), window: Type7.Number(), percent: nullable(Type7.Number()) })),
20542
- agent: Type7.Object({ model: nullable(str2), thinkingLevel: nullable(str2) }),
21591
+ contextUsage: opt(Type7.Object({ tokens: nullable2(Type7.Number()), window: Type7.Number(), percent: nullable2(Type7.Number()) })),
21592
+ agent: Type7.Object({ model: nullable2(str2), thinkingLevel: nullable2(str2) }),
20543
21593
  subagents: Type7.Array(MobileSubagentRoundSchema),
20544
21594
  agents: Type7.Array(MobileAgentSchema),
20545
21595
  approvalCount: int,
@@ -20549,8 +21599,8 @@ var init_protocol = __esm(() => {
20549
21599
  MobileChatSnapshotSchema = Type7.Object({
20550
21600
  ...MobileChatStatusSchema.properties,
20551
21601
  items: Type7.Array(MobileChatItemSchema),
20552
- historyBefore: nullable(str2),
20553
- streaming: nullable(MobileStreamingSchema)
21602
+ historyBefore: nullable2(str2),
21603
+ streaming: nullable2(MobileStreamingSchema)
20554
21604
  });
20555
21605
  MobileWorkspaceSchema = Type7.Object({ root: str2, name: str2 });
20556
21606
  MobileSessionSummarySchema = Type7.Object({
@@ -20583,7 +21633,7 @@ var init_protocol = __esm(() => {
20583
21633
  nextCursor: opt(str2)
20584
21634
  });
20585
21635
  subscription = { subscriptionId: Type7.String({ minLength: 1 }) };
20586
- pageResult = Type7.Object({ items: Type7.Array(MobileChatItemSchema), historyBefore: nullable(str2) });
21636
+ pageResult = Type7.Object({ items: Type7.Array(MobileChatItemSchema), historyBefore: nullable2(str2) });
20587
21637
  pageParams = Type7.Object({
20588
21638
  ...subscription,
20589
21639
  before: Type7.String({ minLength: 1 }),
@@ -20618,6 +21668,34 @@ var init_protocol = __esm(() => {
20618
21668
  params: Type7.Object({ ...subscription, requestId: Type7.String({ minLength: 1, maxLength: 128 }) }),
20619
21669
  result: Type7.Object({ status: Type7.Union([Type7.Literal("accepted"), Type7.Literal("unknown")]) })
20620
21670
  },
21671
+ "chat.steer": {
21672
+ params: Type7.Object({ ...subscription, id: int }),
21673
+ result: Type7.Object({
21674
+ result: Type7.Union([
21675
+ Type7.Literal("steered"),
21676
+ Type7.Literal("withdrawn"),
21677
+ Type7.Literal("placed"),
21678
+ Type7.Literal("settled"),
21679
+ Type7.Literal("unknown")
21680
+ ]),
21681
+ id: opt(int),
21682
+ text: opt(Type7.String()),
21683
+ images: opt(int)
21684
+ })
21685
+ },
21686
+ "chat.withdraw": {
21687
+ params: Type7.Object({ ...subscription, id: int }),
21688
+ result: Type7.Object({
21689
+ result: Type7.Union([
21690
+ Type7.Literal("withdrawn"),
21691
+ Type7.Literal("placed"),
21692
+ Type7.Literal("settled"),
21693
+ Type7.Literal("unknown")
21694
+ ]),
21695
+ text: opt(Type7.String()),
21696
+ images: opt(int)
21697
+ })
21698
+ },
20621
21699
  "chat.abort": { params: Type7.Object(subscription), result: Type7.Object({ accepted: Type7.Boolean() }) },
20622
21700
  "subagents.open": {
20623
21701
  params: Type7.Object({ ...subscription, agentId: opt(Type7.String({ minLength: 1 })), taskId: opt(int) }),
@@ -20680,7 +21758,7 @@ var init_protocol = __esm(() => {
20680
21758
  seq: Type7.Integer({ minimum: 1 }),
20681
21759
  upserts: Type7.Array(MobileChatItemSchema),
20682
21760
  status: opt(MobileChatStatusSchema),
20683
- streaming: opt(nullable(MobileStreamingSchema)),
21761
+ streaming: opt(nullable2(MobileStreamingSchema)),
20684
21762
  streamingAppend: opt(Type7.Object({ text: opt(str2), thinking: opt(str2) }))
20685
21763
  };
20686
21764
  MOBILE_API_V1_NOTIFICATIONS = {
@@ -20877,6 +21955,9 @@ function mapStatus(view, options = {}) {
20877
21955
  const latest = view.compaction?.latest;
20878
21956
  return {
20879
21957
  busy: view.busy,
21958
+ ...view.queued === undefined ? {} : {
21959
+ queued: view.queued.map(({ id, mode, preview, images }) => ({ id, mode, preview, images }))
21960
+ },
20880
21961
  status: view.status,
20881
21962
  usage: mapUsage(view.usage),
20882
21963
  ...view.taskUsage === undefined ? {} : { taskUsage: mapUsage(view.taskUsage) },
@@ -21183,7 +22264,7 @@ function chordToMobileCode(error) {
21183
22264
  return "internal";
21184
22265
  }
21185
22266
  }
21186
- async function remote3(work, invalidAs = "invalid_params") {
22267
+ async function remote2(work, invalidAs = "invalid_params") {
21187
22268
  try {
21188
22269
  return await work();
21189
22270
  } catch (error) {
@@ -21365,6 +22446,10 @@ class MobileAdapter {
21365
22446
  return this.chatSubmit(params);
21366
22447
  case "chat.submissionStatus":
21367
22448
  return this.chatSubmissionStatus(params);
22449
+ case "chat.steer":
22450
+ return this.chatSteer(params);
22451
+ case "chat.withdraw":
22452
+ return this.chatWithdraw(params);
21368
22453
  case "chat.abort":
21369
22454
  return this.chatAbort(params);
21370
22455
  case "subagents.open":
@@ -21397,14 +22482,17 @@ class MobileAdapter {
21397
22482
  }
21398
22483
  }
21399
22484
  async workspacesList() {
21400
- const state = await waitForValue(this.services().workspaces.state);
22485
+ const { workspaces } = this.services();
22486
+ await workspaces.ready;
22487
+ const state = workspaces.state.value;
21401
22488
  return {
21402
22489
  workspaces: state.workspaces.map((workspace) => ({ root: workspace.path, name: baseName(workspace.path) }))
21403
22490
  };
21404
22491
  }
21405
22492
  async registeredWorkspaceRoots() {
21406
- const state = await waitForValue(this.services().workspaces.state);
21407
- return state.workspaces.map((workspace) => workspace.path);
22493
+ const { workspaces } = this.services();
22494
+ await workspaces.ready;
22495
+ return workspaces.state.value.workspaces.map((workspace) => workspace.path);
21408
22496
  }
21409
22497
  async sessionsList(params) {
21410
22498
  const roots = await this.registeredWorkspaceRoots();
@@ -21413,7 +22501,9 @@ class MobileAdapter {
21413
22501
  const offset = params.cursor === undefined ? 0 : Number.parseInt(params.cursor, 10);
21414
22502
  if (!Number.isInteger(offset) || offset < 0)
21415
22503
  throw new MobileMethodError("invalid_params", "invalid cursor");
21416
- const directory = await waitForValue(this.services().directory.state);
22504
+ const { directory: directoryRef } = this.services();
22505
+ await directoryRef.ready;
22506
+ const directory = directoryRef.state.value;
21417
22507
  let sessions = directory.sessions.filter((session) => session.workspaceRoot === params.workspace);
21418
22508
  if (params.query !== undefined) {
21419
22509
  const query = params.query.toLowerCase();
@@ -21439,7 +22529,7 @@ class MobileAdapter {
21439
22529
  if (!roots.includes(params.workspace))
21440
22530
  throw new MobileMethodError("invalid_params", `unknown workspace: ${params.workspace}`);
21441
22531
  this.assertAuthorized();
21442
- const created = await remote3(() => this.services().management.create({ cwd: params.workspace }));
22532
+ const created = await remote2(() => this.services().management.create({ cwd: params.workspace }));
21443
22533
  return {
21444
22534
  id: created.id,
21445
22535
  workspace: created.workspaceRoot,
@@ -21476,16 +22566,13 @@ class MobileAdapter {
21476
22566
  const generation = ++this.attachGeneration;
21477
22567
  this.closeSubagent();
21478
22568
  this.releaseChat();
21479
- const attachment = await remote3(() => services.management.attach(params.sessionId));
22569
+ const attachment = await remote2(() => services.management.attach(params.sessionId));
21480
22570
  if (generation !== this.attachGeneration || this.disposed)
21481
22571
  throw new MobileMethodError("gone", "superseded by a newer attach");
21482
22572
  const chat = services.openChat(attachment);
21483
22573
  const stopView = chat.view.subscribe(() => {});
21484
22574
  try {
21485
- await remote3(async () => {
21486
- await chat.ready;
21487
- await waitForValue(chat.view);
21488
- });
22575
+ await remote2(() => chat.ready);
21489
22576
  } catch (error) {
21490
22577
  stopView();
21491
22578
  chat.release();
@@ -21547,7 +22634,7 @@ class MobileAdapter {
21547
22634
  this.attachGeneration += 1;
21548
22635
  this.closeSubagent();
21549
22636
  this.releaseChat();
21550
- await remote3(() => this.services().management.detach(null)).catch(() => {
22637
+ await remote2(() => this.services().management.detach(null)).catch(() => {
21551
22638
  return;
21552
22639
  });
21553
22640
  return null;
@@ -21578,7 +22665,7 @@ class MobileAdapter {
21578
22665
  const state = this.requireChat(params);
21579
22666
  const busy = state.chat.view.value?.busy ?? false;
21580
22667
  this.assertAuthorized();
21581
- await remote3(() => state.chat.submit({
22668
+ await remote2(() => state.chat.submit({
21582
22669
  content: params.text,
21583
22670
  requestId: params.requestId,
21584
22671
  ...params.mode === "steer" ? { whenBusy: "steer" } : {}
@@ -21592,10 +22679,20 @@ class MobileAdapter {
21592
22679
  const accepted = await this.runtime.submissionAccepted(state.sessionId, params.requestId);
21593
22680
  return { status: accepted ? "accepted" : "unknown" };
21594
22681
  }
22682
+ async chatSteer(params) {
22683
+ const state = this.requireChat(params);
22684
+ this.assertAuthorized();
22685
+ return remote2(() => state.chat.steer({ id: params.id }), "rejected");
22686
+ }
22687
+ async chatWithdraw(params) {
22688
+ const state = this.requireChat(params);
22689
+ this.assertAuthorized();
22690
+ return remote2(() => state.chat.withdraw({ id: params.id }), "rejected");
22691
+ }
21595
22692
  async chatAbort(params) {
21596
22693
  const state = this.requireChat(params);
21597
22694
  this.assertAuthorized();
21598
- await remote3(() => state.chat.abort(null));
22695
+ await remote2(() => state.chat.abort(null));
21599
22696
  return { accepted: true };
21600
22697
  }
21601
22698
  async approvalRecords(state) {
@@ -21674,7 +22771,7 @@ class MobileAdapter {
21674
22771
  this.assertAuthorized();
21675
22772
  const handle = openSubagent({ ...state.attachment, taskId: params.taskId });
21676
22773
  try {
21677
- const status = await remote3(() => handle.decideApproval({
22774
+ const status = await remote2(() => handle.decideApproval({
21678
22775
  id: params.id,
21679
22776
  version: params.version,
21680
22777
  decision: params.decision === "allow" ? "allowed" : "denied"
@@ -21754,7 +22851,7 @@ class MobileAdapter {
21754
22851
  return () => listeners.delete(listener);
21755
22852
  },
21756
22853
  older: async (before) => {
21757
- const page = await remote3(() => client.history({ before }));
22854
+ const page = await remote2(() => client.history({ before }));
21758
22855
  return { items: page.items, next: page.next };
21759
22856
  }
21760
22857
  };
@@ -21798,7 +22895,7 @@ class MobileAdapter {
21798
22895
  this.assertAuthorized();
21799
22896
  const handle = openSubagent({ ...chat.attachment, taskId: params.taskId });
21800
22897
  try {
21801
- await remote3(() => handle.stop({ taskId: params.taskId }));
22898
+ await remote2(() => handle.stop({ taskId: params.taskId }));
21802
22899
  } finally {
21803
22900
  handle.release();
21804
22901
  }
@@ -21812,9 +22909,9 @@ class MobileAdapter {
21812
22909
  const state = this.subagentState;
21813
22910
  if (state === undefined || state.agentId !== params.agentId || state.chatSubscriptionId !== chat.subscriptionId)
21814
22911
  throw new MobileMethodError("gone", "subagent view is no longer open");
21815
- chunk = await remote3(() => state.client.getImage(input));
22912
+ chunk = await remote2(() => state.client.getImage(input));
21816
22913
  } else {
21817
- chunk = await remote3(() => chat.chat.getImage(input));
22914
+ chunk = await remote2(() => chat.chat.getImage(input));
21818
22915
  }
21819
22916
  const bytes = Buffer.from(chunk.data, "base64").subarray(0, MOBILE_LIMITS.chunkBytes);
21820
22917
  const nextOffset = params.offset + bytes.length;
@@ -21864,7 +22961,6 @@ class MobileAdapter {
21864
22961
  var CLOSE_INVALID_JSON = 1003, CLOSE_PROTOCOL_ERROR = 1002, ACTIVE_ROUND, MobileMethodError;
21865
22962
  var init_adapter = __esm(() => {
21866
22963
  init_src();
21867
- init_client_session();
21868
22964
  init_projection2();
21869
22965
  init_protocol();
21870
22966
  ACTIVE_ROUND = new Set(["queued", "running"]);
@@ -39188,7 +40284,7 @@ var init_wsl = __esm(() => {
39188
40284
  import { randomUUID as randomUUID2 } from "crypto";
39189
40285
  import { createServer as createServer2 } from "https";
39190
40286
  import { hostname } from "os";
39191
- import { createHost as createHost2, memoryLinkPair } from "@earendil-works/chord";
40287
+ import { memoryLinkPair } from "@earendil-works/chord";
39192
40288
  import { WebSocketServer as WebSocketServer2 } from "ws";
39193
40289
  function defaultServerName() {
39194
40290
  const name = hostname().trim();
@@ -39220,9 +40316,6 @@ function describeRelayError(error, address, port, bindAddress) {
39220
40316
  return `Windows \u4E2D\u7EE7\u542F\u52A8\u5931\u8D25\uFF1A${error.message}`;
39221
40317
  }
39222
40318
  }
39223
- function isMobilePrincipal2(principal) {
39224
- return typeof principal === "object" && principal !== null && principal.kind === "mobile";
39225
- }
39226
40319
  function toMessage(error) {
39227
40320
  if (error instanceof Error) {
39228
40321
  const code = error.code;
@@ -39401,7 +40494,7 @@ class MobileGateway {
39401
40494
  await store.clearMobileAuthorizations();
39402
40495
  this.certificate = certificate;
39403
40496
  this.pairingValue?.invalidate();
39404
- await this.chord.invalidatePrincipal(isMobilePrincipal2);
40497
+ await this.chord.invalidatePrincipal(isMobilePrincipal);
39405
40498
  for (const connection of [...this.connections.values()])
39406
40499
  connection.close(MOBILE_CLOSE_REVOKED, "revoked");
39407
40500
  const current = this.stateValue;
@@ -39477,11 +40570,7 @@ class MobileGateway {
39477
40570
  store.rememberConnectedAddress(learned).catch((error) => this.onError?.(toError(error)));
39478
40571
  }
39479
40572
  }
39480
- const clientHost = createHost2({
39481
- kind: "client",
39482
- upstream: identifiedLink(clientEnd, this.serverId),
39483
- onError: () => {}
39484
- });
40573
+ const clientHost = createClientHost(clientEnd);
39485
40574
  let servicesValue;
39486
40575
  const adapter = new MobileAdapter({
39487
40576
  socket,
@@ -39493,11 +40582,11 @@ class MobileGateway {
39493
40582
  services: () => {
39494
40583
  if (servicesValue === undefined) {
39495
40584
  servicesValue = {
39496
- directory: clientHost.get(NekoSessionDirectory, {}, { at: this.serverId }),
39497
- management: clientHost.get(NekoSessionManagement, {}, { at: this.serverId }),
39498
- workspaces: clientHost.get(NekoWorkspaces, {}, { at: this.serverId }),
39499
- openChat: (attachment) => clientHost.get(NekoChat, attachment, { at: this.serverId }),
39500
- openSubagent: (input) => clientHost.get(NekoSubagent, input, { at: this.serverId })
40585
+ directory: clientHost.hold(NekoSessionDirectory, {}),
40586
+ management: clientHost.hold(NekoSessionManagement, {}),
40587
+ workspaces: clientHost.hold(NekoWorkspaces, {}),
40588
+ openChat: (attachment) => clientHost.hold(NekoChat, attachment),
40589
+ openSubagent: (input) => clientHost.hold(NekoSubagent, input)
39501
40590
  };
39502
40591
  }
39503
40592
  return servicesValue;
@@ -39521,10 +40610,13 @@ class MobileGateway {
39521
40610
  if (this.notificationCleanup !== undefined)
39522
40611
  return;
39523
40612
  const [serverEnd, clientEnd] = memoryLinkPair();
39524
- this.chord.accept(serverEnd, { principal: { connectionId: "mobile-notification-hub" }, kind: "client" });
39525
- const host = createHost2({ kind: "client", upstream: identifiedLink(clientEnd, this.serverId), onError: () => {} });
39526
- const directory = host.get(NekoSessionDirectory, {}, { at: this.serverId });
39527
- const workspaces = host.get(NekoWorkspaces, {}, { at: this.serverId });
40613
+ this.chord.accept(serverEnd, {
40614
+ principal: { kind: "local", connectionId: "mobile-notification-hub" },
40615
+ kind: "client"
40616
+ });
40617
+ const host = createClientHost(clientEnd);
40618
+ const directory = host.hold(NekoSessionDirectory, {});
40619
+ const workspaces = host.hold(NekoWorkspaces, {});
39528
40620
  const last = { directory: -1, workspaces: -1 };
39529
40621
  const unsubscribe = [
39530
40622
  directory.state.subscribe(() => {
@@ -39808,6 +40900,7 @@ var RELAY_PORT_RETRY_MS = 1e4, RELAY_PORT_RETRY_INTERVAL_MS = 1000, WSL_INTEROP_
39808
40900
  var init_listener = __esm(() => {
39809
40901
  init_contract();
39810
40902
  init_link();
40903
+ init_principal();
39811
40904
  init_adapter();
39812
40905
  init_auth();
39813
40906
  init_certificate();
@@ -39942,15 +41035,7 @@ var init_image_uploads = __esm(() => {
39942
41035
  });
39943
41036
 
39944
41037
  // packages/gateway/src/subagent-host.ts
39945
- import {
39946
- ChordError as ChordError3,
39947
- cell,
39948
- hostPrincipal,
39949
- listener,
39950
- local,
39951
- opaque,
39952
- resource
39953
- } from "@earendil-works/chord";
41038
+ import { ChordError as ChordError3, local, opaque } from "@earendil-works/chord";
39954
41039
  import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
39955
41040
  import { Type as Type8 } from "typebox";
39956
41041
  async function openReader(runtime, childId, parentId, context, through, historyUsage) {
@@ -40077,31 +41162,27 @@ async function openReader(runtime, childId, parentId, context, through, historyU
40077
41162
  }
40078
41163
  function installSubagentHost(host, runtime, context) {
40079
41164
  host.serve(ChildProjection, (input, _principal, self2) => {
40080
- const { revision } = self2.state({ revision: cell(Type8.Integer(), 0) });
40081
- const { reader } = self2.resources({
40082
- reader: resource({
40083
- rev: 1,
40084
- async open() {
40085
- const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => input.agentId.startsWith("round-") ? d.taskId === Number(input.agentId.slice(6)) : (d.agentId ?? agentIdFor(d.childId)) === input.agentId);
40086
- if (!entry)
40087
- throw new ChordError3("gone", "Subagent unavailable");
40088
- const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
40089
- try {
40090
- const initial = await r.refresh();
40091
- await host.root((ctx) => revision.replace(ctx, initial));
40092
- return r;
40093
- } catch (error) {
40094
- r.close();
40095
- throw error;
40096
- }
40097
- },
40098
- close: (r) => r.close()
40099
- })
41165
+ const revision = self2.state("revision", Type8.Integer(), 0);
41166
+ const reader = self2.resource("reader", {
41167
+ version: 1,
41168
+ async open() {
41169
+ const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => input.agentId.startsWith("round-") ? d.taskId === Number(input.agentId.slice(6)) : (d.agentId ?? agentIdFor(d.childId)) === input.agentId);
41170
+ if (!entry)
41171
+ throw new ChordError3("gone", "Subagent unavailable");
41172
+ const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
41173
+ try {
41174
+ const initial = await r.refresh();
41175
+ await host.root((ctx) => revision.replace(ctx, initial));
41176
+ return r;
41177
+ } catch (error) {
41178
+ r.close();
41179
+ throw error;
41180
+ }
41181
+ },
41182
+ close: (r) => r.close()
40100
41183
  });
40101
- self2.listen({
40102
- view: listener(reader, (r) => r, async (_event, ctx) => {
40103
- revision.replace(ctx, await reader.value.refresh());
40104
- })
41184
+ self2.on("view", reader, (r) => r, async (_event, ctx) => {
41185
+ revision.replace(ctx, await reader.value.refresh());
40105
41186
  });
40106
41187
  return { revision, reader: self2.expose(reader) };
40107
41188
  });
@@ -40117,23 +41198,24 @@ function installSubagentHost(host, runtime, context) {
40117
41198
  throw new ChordError3("forbidden", "Subagent does not belong to this session");
40118
41199
  return entry;
40119
41200
  };
40120
- const { target } = self2.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
40121
- const shared = self2.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
40122
- const { authorizedRevision } = self2.state({ authorizedRevision: cell(Type8.Integer(), 0) });
40123
- self2.listen({
40124
- authorizedView: listener(shared.revision, async (value, ctx) => {
40125
- try {
40126
- target.value;
40127
- await check();
40128
- if (value)
40129
- authorizedRevision.replace(ctx, value);
40130
- } catch {
40131
- authorizedRevision.replace(ctx, 0);
40132
- }
40133
- })
41201
+ const target = self2.resource("target", { version: 1, deps: [owner], open: check });
41202
+ const shared = self2.get(ChildProjection, {
41203
+ parentId: Number(input.sessionId),
41204
+ agentId: input.agentId ?? `round-${input.taskId}`
41205
+ });
41206
+ const authorizedRevision = self2.state("authorizedRevision", Type8.Integer(), 0);
41207
+ self2.on("authorizedView", shared.revision, async (value, ctx) => {
41208
+ try {
41209
+ target.value;
41210
+ await check();
41211
+ if (value)
41212
+ authorizedRevision.replace(ctx, value);
41213
+ } catch {
41214
+ authorizedRevision.replace(ctx, 0);
41215
+ }
40134
41216
  });
40135
41217
  return {
40136
- kind: "ready",
41218
+ variant: "ready",
40137
41219
  revision: authorizedRevision,
40138
41220
  chunk: async (request) => {
40139
41221
  await check();
@@ -40193,12 +41275,8 @@ import { open as open3 } from "fs/promises";
40193
41275
  import { basename as basename5, extname } from "path";
40194
41276
  import {
40195
41277
  ChordError as ChordError4,
40196
- cell as cell2,
40197
- hostPrincipal as hostPrincipal2,
40198
- listener as listener2,
40199
41278
  local as local2,
40200
- opaque as opaque2,
40201
- resource as resource2
41279
+ opaque as opaque2
40202
41280
  } from "@earendil-works/chord";
40203
41281
  import { withAbortSignal as withAbortSignal4 } from "@earendil-works/chord/context";
40204
41282
  import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
@@ -40292,65 +41370,61 @@ async function openProjection(runtime, session, context) {
40292
41370
  function serveSessions(host, runtime, context) {
40293
41371
  installSubagentHost(host, runtime, context);
40294
41372
  host.serve(Owner, (_input, _principal, self2) => {
40295
- const { owner } = self2.resources({
40296
- owner: resource2({
40297
- rev: 1,
40298
- open: () => ({ active: null }),
40299
- close: (value) => {
40300
- value.active = null;
40301
- }
40302
- })
41373
+ const owner = self2.resource("owner", {
41374
+ version: 1,
41375
+ open: () => ({ active: null }),
41376
+ close: (value) => {
41377
+ value.active = null;
41378
+ }
40303
41379
  });
40304
41380
  return { owner: self2.expose(owner) };
40305
41381
  });
40306
41382
  host.serve(Projection, (input, _principal, self2) => {
40307
- const { view } = self2.state({ view: cell2(ChatViewSchema, emptyView) });
40308
- const { projection } = self2.resources({
40309
- projection: resource2({
40310
- rev: 1,
40311
- async open({ signal }) {
40312
- const session = await runtime.getConversation(Number(input.id));
40313
- if (!session)
40314
- throw new ChordError4("gone", "Session unavailable");
40315
- const opened = await openProjection(runtime, session, context);
40316
- try {
40317
- const initial = await opened.project();
40318
- await host.root((ctx) => view.replace(ctx, initial), { signal });
40319
- return opened;
40320
- } catch (error) {
40321
- opened.close();
40322
- throw error;
40323
- }
40324
- },
40325
- close: (value) => value.close()
40326
- })
41383
+ const view = self2.state("view", ChatViewSchema, emptyView);
41384
+ const projection = self2.resource("projection", {
41385
+ version: 1,
41386
+ async open({ signal }) {
41387
+ const session = await runtime.getConversation(Number(input.id));
41388
+ if (!session)
41389
+ throw new ChordError4("gone", "Session unavailable");
41390
+ const opened = await openProjection(runtime, session, context);
41391
+ try {
41392
+ const initial = await opened.project();
41393
+ await host.root((ctx) => view.replace(ctx, initial), { signal });
41394
+ return opened;
41395
+ } catch (error) {
41396
+ opened.close();
41397
+ throw error;
41398
+ }
41399
+ },
41400
+ close: (value) => value.close()
40327
41401
  });
40328
- self2.listen({
40329
- view: listener2(projection, (value) => value, async (_event, ctx) => {
40330
- view.replace(ctx, await projection.value.project());
40331
- })
41402
+ self2.on("view", projection, (value) => value, async (_event, ctx) => {
41403
+ view.replace(ctx, await projection.value.project());
40332
41404
  });
40333
41405
  return { view, reader: self2.expose(projection) };
40334
41406
  });
40335
41407
  host.serve(NekoChat, (input, principal, self2) => {
40336
41408
  const owner = self2.get(Owner, {}, { principal });
40337
- const shared = self2.get(Projection, { id: input.sessionId }, { principal: hostPrincipal2 });
40338
- const { uploads, projection } = self2.resources({
40339
- uploads: resource2({ rev: 1, open: () => new ImageUploads, close: (value) => value.close() }),
40340
- projection: resource2({
40341
- rev: 1,
40342
- deps: [owner],
40343
- async open() {
40344
- if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
40345
- throw new ChordError4("forbidden", "Attachment does not belong to this connection");
40346
- const session = await runtime.getConversation(Number(input.sessionId));
40347
- if (!session)
40348
- throw new ChordError4("gone", "Session unavailable");
40349
- return {
40350
- session
40351
- };
40352
- }
40353
- })
41409
+ const shared = self2.get(Projection, { id: input.sessionId });
41410
+ const uploads = self2.resource("uploads", {
41411
+ version: 1,
41412
+ open: () => new ImageUploads,
41413
+ close: (value) => value.close()
41414
+ });
41415
+ const projection = self2.resource("projection", {
41416
+ version: 1,
41417
+ deps: [owner],
41418
+ async open() {
41419
+ if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
41420
+ throw new ChordError4("forbidden", "Attachment does not belong to this connection");
41421
+ const session = await runtime.getConversation(Number(input.sessionId));
41422
+ if (!session)
41423
+ throw new ChordError4("gone", "Session unavailable");
41424
+ return {
41425
+ session
41426
+ };
41427
+ }
40354
41428
  });
40355
41429
  const safeUpload = (action) => {
40356
41430
  try {
@@ -40360,7 +41434,7 @@ function serveSessions(host, runtime, context) {
40360
41434
  }
40361
41435
  };
40362
41436
  const behavior = {
40363
- kind: "ready",
41437
+ variant: "ready",
40364
41438
  view: shared.view,
40365
41439
  draftImage: async ({ id, offset }) => safeUpload(() => uploads.value.read(id, offset)),
40366
41440
  importImage: async (path, ctx) => {
@@ -40467,6 +41541,8 @@ function serveSessions(host, runtime, context) {
40467
41541
  await projection.value.session.abort();
40468
41542
  return null;
40469
41543
  },
41544
+ withdraw: ({ id }) => projection.value.session.withdraw(id),
41545
+ steer: ({ id }) => projection.value.session.steer(id),
40470
41546
  skills: () => runtime.listSkills(projection.value.session.id),
40471
41547
  jobOutput: async ({ jobId, since }) => {
40472
41548
  try {
@@ -40544,6 +41620,7 @@ var init_session_host = __esm(() => {
40544
41620
  agent: { model: null, thinkingLevel: null },
40545
41621
  cwd: "",
40546
41622
  busy: false,
41623
+ queued: [],
40547
41624
  status: null,
40548
41625
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 },
40549
41626
  items: [],
@@ -41224,20 +42301,14 @@ var init_workspaces = __esm(() => {
41224
42301
 
41225
42302
  // packages/gateway/src/server-services.ts
41226
42303
  import { randomUUID as randomUUID4 } from "crypto";
41227
- import {
41228
- ChordError as ChordError5,
41229
- cell as cell3,
41230
- hostPrincipal as hostPrincipal3,
41231
- listener as listener3,
41232
- local as local3,
41233
- method as method3
41234
- } from "@earendil-works/chord";
42304
+ import { ChordError as ChordError5, local as local3, method as method2 } from "@earendil-works/chord";
41235
42305
  import { Type as Type10 } from "typebox";
41236
42306
  async function createNekoServerServices(options) {
41237
42307
  const { runtime, context, host } = options;
41238
42308
  const updates = new UpdateChecker(options.updates);
41239
42309
  await updates.local();
41240
42310
  const updateListeners = new Set;
42311
+ const info = (value) => ({ ...value, serverId: options.serverId });
41241
42312
  let published = JSON.stringify(updates.state), closed = false;
41242
42313
  const publishUpdates = () => {
41243
42314
  if (closed)
@@ -41250,22 +42321,20 @@ async function createNekoServerServices(options) {
41250
42321
  listener(updates.state);
41251
42322
  };
41252
42323
  host.serve(NekoApplicationInfo, (_input, _principal, self2) => {
41253
- const state = self2.state({ info: cell3(ApplicationInfoSchema, updates.state) });
41254
- self2.listen({
41255
- updates: listener3({
41256
- subscribe: (fn) => {
41257
- updateListeners.add(fn);
41258
- return () => updateListeners.delete(fn);
41259
- }
41260
- }, (value, ctx) => state.info.replace(ctx, value))
41261
- });
42324
+ const current = self2.state("info", ApplicationInfoSchema, info(updates.state));
42325
+ self2.on("updates", {
42326
+ subscribe: (fn) => {
42327
+ updateListeners.add(fn);
42328
+ return () => updateListeners.delete(fn);
42329
+ }
42330
+ }, (value, ctx) => current.replace(ctx, info(value)));
41262
42331
  return {
41263
- kind: "ready",
41264
- info: state.info,
42332
+ variant: "ready",
42333
+ info: current,
41265
42334
  check: async () => {
41266
42335
  await updates.check(true);
41267
42336
  publishUpdates();
41268
- return updates.state;
42337
+ return info(updates.state);
41269
42338
  }
41270
42339
  };
41271
42340
  });
@@ -41309,7 +42378,13 @@ async function createNekoServerServices(options) {
41309
42378
  status: info.status
41310
42379
  }));
41311
42380
  const readStatuses = async () => Object.fromEntries(await Promise.all(CREDENTIAL_PROVIDERS.map(async (spec) => [spec.id, await runtime.credentialStatus(spec.id)])));
41312
- const [sessions, workspaces, statuses] = await Promise.all([list(), runtime.listWorkspaces(), readStatuses()]);
42381
+ const [sessions, workspaces, statuses, search, contextSettings0] = await Promise.all([
42382
+ list(),
42383
+ runtime.listWorkspaces(),
42384
+ readStatuses(),
42385
+ runtime.searchSettings(),
42386
+ runtime.contextSettings()
42387
+ ]);
41313
42388
  let mutationTail = Promise.resolve();
41314
42389
  const serialize = (fn) => {
41315
42390
  const result = mutationTail.catch(() => {}).then(fn);
@@ -41317,43 +42392,64 @@ async function createNekoServerServices(options) {
41317
42392
  return result;
41318
42393
  };
41319
42394
  host.serve(ServerState, (_input, _principal, self2) => {
41320
- const state = self2.state({
41321
- directory: cell3(DirectoryStateSchema, { revision: 1, sessions }),
41322
- workspaces: cell3(WorkspaceStateSchema, { revision: 1, workspaces }),
41323
- credentials: cell3(CredentialStateSchema, { revision: 1, statuses })
42395
+ const directory = self2.state("directory", DirectoryStateSchema, { revision: 1, sessions });
42396
+ const workspaceState = self2.state("workspaces", WorkspaceStateSchema, { revision: 1, workspaces });
42397
+ const credentials = self2.state("credentials", CredentialStateSchema, { revision: 1, statuses });
42398
+ const searchSettings = self2.state("searchSettings", SearchSettingsStateSchema, { revision: 1, ...search });
42399
+ const contextSettings = self2.state("contextSettings", ContextSettingsStateSchema, {
42400
+ revision: 1,
42401
+ ...contextSettings0
41324
42402
  });
41325
- self2.listen({
41326
- directory: listener3({ subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
41327
- if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
41328
- "app.neko.session-metadata",
41329
- "app.neko.activity",
41330
- "app.neko.conversation",
41331
- "app.neko.workspaces",
41332
- "pi.agent"
41333
- ].includes(change.record.kind)))
41334
- return;
41335
- await serialize(async () => {
41336
- const [items, roots] = await Promise.all([list(), runtime.listWorkspaces()]);
41337
- state.directory.replace(ctx, { revision: state.directory.value.revision + 1, sessions: items });
41338
- state.workspaces.replace(ctx, { revision: state.workspaces.value.revision + 1, workspaces: roots });
41339
- });
41340
- })
42403
+ const refreshContextSettings = async (_input, ctx) => {
42404
+ const next = await runtime.contextSettings();
42405
+ const { revision, ...current } = contextSettings.value;
42406
+ if (JSON.stringify(next) !== JSON.stringify(current))
42407
+ contextSettings.replace(ctx, { revision: revision + 1, ...next });
42408
+ return null;
42409
+ };
42410
+ const refreshSearchSettings = async (_input, ctx) => {
42411
+ const next = await runtime.searchSettings();
42412
+ const current = searchSettings.value;
42413
+ if (next.selected !== current.selected)
42414
+ searchSettings.replace(ctx, { revision: current.revision + 1, ...next });
42415
+ return null;
42416
+ };
42417
+ self2.on("directory", { subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
42418
+ if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
42419
+ "app.neko.session-metadata",
42420
+ "app.neko.activity",
42421
+ "app.neko.conversation",
42422
+ "app.neko.workspaces",
42423
+ "pi.agent"
42424
+ ].includes(change.record.kind)))
42425
+ return;
42426
+ await serialize(async () => {
42427
+ const [items, roots] = await Promise.all([list(), runtime.listWorkspaces()]);
42428
+ directory.replace(ctx, { revision: directory.value.revision + 1, sessions: items });
42429
+ workspaceState.replace(ctx, { revision: workspaceState.value.revision + 1, workspaces: roots });
42430
+ });
41341
42431
  });
41342
42432
  return {
41343
- ...state,
42433
+ directory,
42434
+ workspaces: workspaceState,
42435
+ credentials,
42436
+ searchSettings,
42437
+ contextSettings,
41344
42438
  async refreshCredentials(_input, ctx) {
41345
- state.credentials.replace(ctx, {
41346
- revision: state.credentials.value.revision + 1,
42439
+ credentials.replace(ctx, {
42440
+ revision: credentials.value.revision + 1,
41347
42441
  statuses: await readStatuses()
41348
42442
  });
41349
42443
  return null;
41350
- }
42444
+ },
42445
+ refreshSearchSettings,
42446
+ refreshContextSettings
41351
42447
  };
41352
- }, { eager: { input: {} } });
41353
- const shared = (self2) => self2.get(ServerState, {}, { principal: hostPrincipal3 });
41354
- host.serve(NekoSessionDirectory, (_input, _principal, self2) => ({ kind: "ready", state: shared(self2).directory }));
42448
+ }, { lifetime: "host" });
42449
+ const shared = (self2) => self2.get(ServerState, {});
42450
+ host.serve(NekoSessionDirectory, (_input, _principal, self2) => ({ variant: "ready", state: shared(self2).directory }));
41355
42451
  host.serve(NekoWorkspaces, (_input, _principal, self2) => ({
41356
- kind: "ready",
42452
+ variant: "ready",
41357
42453
  state: shared(self2).workspaces,
41358
42454
  browse: (input) => browseWorkspaces(input),
41359
42455
  add: (input) => serialize(() => runtime.addWorkspace(input.path))
@@ -41361,7 +42457,7 @@ async function createNekoServerServices(options) {
41361
42457
  host.serve(NekoCredentials, (_input, _principal, self2) => {
41362
42458
  const hub = shared(self2);
41363
42459
  return {
41364
- kind: "ready",
42460
+ variant: "ready",
41365
42461
  state: hub.credentials,
41366
42462
  save: async (input, ctx) => {
41367
42463
  await runtime.saveCredential(input.provider, input.key);
@@ -41373,8 +42469,34 @@ async function createNekoServerServices(options) {
41373
42469
  }
41374
42470
  };
41375
42471
  });
42472
+ host.serve(NekoSearchSettings, (_input, _principal, self2) => {
42473
+ const hub = shared(self2);
42474
+ return {
42475
+ variant: "ready",
42476
+ state: hub.searchSettings,
42477
+ set: (input, ctx) => serialize(async () => {
42478
+ await runtime.setSearchProvider(input.provider);
42479
+ return hub.refreshSearchSettings(null, ctx);
42480
+ })
42481
+ };
42482
+ });
42483
+ host.serve(NekoContextSettings, (_input, _principal, self2) => {
42484
+ const hub = shared(self2);
42485
+ return {
42486
+ variant: "ready",
42487
+ state: hub.contextSettings,
42488
+ set: (input, ctx) => serialize(async () => {
42489
+ await runtime.setContextSettings(input);
42490
+ return hub.refreshContextSettings(null, ctx);
42491
+ }),
42492
+ reset: (_input, ctx) => serialize(async () => {
42493
+ await runtime.resetContextSettings();
42494
+ return hub.refreshContextSettings(null, ctx);
42495
+ })
42496
+ };
42497
+ });
41376
42498
  host.serve(NekoModels, () => ({
41377
- kind: "ready",
42499
+ variant: "ready",
41378
42500
  list: async () => [...runtime.listModels()].map((value) => ({
41379
42501
  ...value,
41380
42502
  input: [...value.input],
@@ -41382,7 +42504,7 @@ async function createNekoServerServices(options) {
41382
42504
  }))
41383
42505
  }));
41384
42506
  host.serve(NekoWorkspaceFiles, () => ({
41385
- kind: "ready",
42507
+ variant: "ready",
41386
42508
  search: async (input, ctx) => {
41387
42509
  const roots = await runtime.listWorkspaces();
41388
42510
  if (!roots.some((root) => root.path === input.workspaceRoot))
@@ -41400,7 +42522,7 @@ async function createNekoServerServices(options) {
41400
42522
  return found;
41401
42523
  };
41402
42524
  return {
41403
- kind: "ready",
42525
+ variant: "ready",
41404
42526
  create: (input) => serialize(async () => summary((await runtime.createConversation(input)).id)),
41405
42527
  fork: (id) => serialize(async () => summary((await runtime.forkConversation(toConversationId(id))).id)),
41406
42528
  archive: async (id) => {
@@ -41423,7 +42545,7 @@ async function createNekoServerServices(options) {
41423
42545
  };
41424
42546
  });
41425
42547
  host.serve(NekoSkillMarket, () => ({
41426
- kind: "ready",
42548
+ variant: "ready",
41427
42549
  list: (input) => market.list(input.query).then((skills) => Promise.all(skills.map((skill) => withInstallState(skill, input.workspaceRoot)))).catch(toRemote),
41428
42550
  detail: (input) => market.detail(input.name).then((skill) => withInstallState(skill, input.workspaceRoot)).catch(toRemote),
41429
42551
  preview: (input) => market.preview(input.name).then((value) => ({ ...value, files: [...value.files] })).catch(toRemote),
@@ -41465,7 +42587,11 @@ var init_server_services = __esm(() => {
41465
42587
  directory: DirectoryStateSchema,
41466
42588
  workspaces: WorkspaceStateSchema,
41467
42589
  credentials: CredentialStateSchema,
41468
- refreshCredentials: method3(Type10.Null(), Type10.Null())
42590
+ searchSettings: SearchSettingsStateSchema,
42591
+ contextSettings: ContextSettingsStateSchema,
42592
+ refreshCredentials: method2(Type10.Null(), Type10.Null()),
42593
+ refreshSearchSettings: method2(Type10.Null(), Type10.Null()),
42594
+ refreshContextSettings: method2(Type10.Null(), Type10.Null())
41469
42595
  }
41470
42596
  });
41471
42597
  });
@@ -41474,7 +42600,7 @@ var init_server_services = __esm(() => {
41474
42600
  import { randomUUID as randomUUID5 } from "crypto";
41475
42601
  import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
41476
42602
  import { basename as basename6, dirname as dirname10, join as join14 } from "path";
41477
- import { createHost as createHost3 } from "@earendil-works/chord";
42603
+ import { createHost as createHost2 } from "@earendil-works/chord";
41478
42604
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
41479
42605
  async function startGateway(options = {}) {
41480
42606
  const context = options.context ?? BACKGROUND_CONTEXT4;
@@ -41483,6 +42609,7 @@ async function startGateway(options = {}) {
41483
42609
  const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
41484
42610
  const host = options.host ?? fileConfig.gateway?.host ?? DEFAULT_HOST;
41485
42611
  const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? (process.platform === "win32" ? 0 : DEFAULT_PORT);
42612
+ const searchProvider = options.searchProvider ?? fileConfig.webSearch?.provider;
41486
42613
  const marketUrl = options.marketUrl ?? fileConfig.skills?.marketUrl ?? DEFAULT_SKILL_MARKET_URL;
41487
42614
  const nekoDir = dirname10(storagePath);
41488
42615
  const serverId = options.serverId ?? await loadOrCreateServerId(nekoDir);
@@ -41494,6 +42621,8 @@ async function startGateway(options = {}) {
41494
42621
  ...options.storage !== "memory" ? { storagePath } : {},
41495
42622
  ...options.credentialsPath !== undefined ? { credentialsPath: options.credentialsPath } : {},
41496
42623
  ...options.apiKey !== undefined ? { apiKey: options.apiKey } : {},
42624
+ ...searchProvider !== undefined ? { searchProvider } : {},
42625
+ ...fileConfig.context !== undefined ? { contextSettings: fileConfig.context } : {},
41497
42626
  model,
41498
42627
  thinkingLevel: options.thinkingLevel ?? fileConfig.thinkingLevel ?? "high",
41499
42628
  ...options.cwd !== undefined ? { cwd: options.cwd } : {},
@@ -41524,8 +42653,9 @@ async function startGateway(options = {}) {
41524
42653
  }
41525
42654
  async function bindGateway(runtime, options) {
41526
42655
  const { context, host, port, socketPath, serverId, nekoDir } = options;
41527
- const chord = createHost3({
41528
- name: serverId,
42656
+ const chord = createHost2({
42657
+ name: GATEWAY_HOST,
42658
+ self: HOST_PRINCIPAL,
41529
42659
  dev: true,
41530
42660
  onError: (error) => options.onError?.(error instanceof Error ? error : new Error(String(error))),
41531
42661
  principalKey: gatewayPrincipalKey
@@ -41534,6 +42664,7 @@ async function bindGateway(runtime, options) {
41534
42664
  host: chord,
41535
42665
  runtime,
41536
42666
  context,
42667
+ serverId,
41537
42668
  marketUrl: options.marketUrl,
41538
42669
  ...options.updates ? { updates: options.updates } : {},
41539
42670
  ...options.updateIntervalMs ? { updateIntervalMs: options.updateIntervalMs } : {},
@@ -41549,7 +42680,10 @@ async function bindGateway(runtime, options) {
41549
42680
  }
41550
42681
  try {
41551
42682
  for (const listener of listeners)
41552
- await listener.start((link) => chord.accept(link, { principal: { connectionId: randomUUID5() } }));
42683
+ await listener.start((link) => chord.accept(link, {
42684
+ principal: { kind: "local", connectionId: randomUUID5() },
42685
+ onProtocolMismatch: ({ peer }) => options.onError?.(new Error(`Rejected a client on Chord protocol ${peer ?? "unknown"}; restart or update it.`))
42686
+ }));
41553
42687
  } catch (error) {
41554
42688
  await Promise.allSettled(listeners.map((listener) => listener.close()));
41555
42689
  await chord.close();
@@ -41612,16 +42746,6 @@ async function bindGateway(runtime, options) {
41612
42746
  }).catch((error) => options.onError?.(error instanceof Error ? error : new Error(String(error))));
41613
42747
  return gateway;
41614
42748
  }
41615
- function gatewayPrincipalKey(principal) {
41616
- if (typeof principal === "object" && principal !== null) {
41617
- const record = principal;
41618
- if (record.kind === "mobile" && typeof record.deviceId === "string" && typeof record.grantRevision === "number") {
41619
- const connectionId = typeof record.connectionId === "string" ? record.connectionId : "";
41620
- return `mobile:${record.deviceId}:${record.grantRevision}:${connectionId}`;
41621
- }
41622
- }
41623
- return JSON.stringify(principal) ?? "unknown";
41624
- }
41625
42749
  function toStartupError(error, info) {
41626
42750
  const message = error instanceof Error ? error.message : String(error);
41627
42751
  const existingServerId = info.socketPath === null ? undefined : serverIdFromSocketPath(info.socketPath);
@@ -41661,9 +42785,11 @@ async function loadOrCreateServerId(nekoDir) {
41661
42785
  var DEFAULT_HOST = "127.0.0.1", DEFAULT_PORT = 8321, SERVER_ID_FILENAME = "gateway-server-id";
41662
42786
  var init_gateway = __esm(() => {
41663
42787
  init_src();
42788
+ init_contract();
41664
42789
  init_admin();
41665
42790
  init_listener();
41666
42791
  init_port_file();
42792
+ init_principal();
41667
42793
  init_server_services();
41668
42794
  init_unix_link();
41669
42795
  init_ws_listener();
@@ -41768,6 +42894,7 @@ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5000;
41768
42894
  // packages/gateway/src/index.ts
41769
42895
  var init_src2 = __esm(() => {
41770
42896
  init_client();
42897
+ init_client_session();
41771
42898
  init_contract();
41772
42899
  init_gateway();
41773
42900
  init_link();
@@ -41838,7 +42965,7 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
41838
42965
  const timeoutMs = plan.spawnTimeoutMs ?? 20000;
41839
42966
  if (plan.platform === "win32") {
41840
42967
  const timeoutAt = Date.now() + timeoutMs;
41841
- const serverId = plan.fileServerId ?? await waitForValue2(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
42968
+ const serverId = plan.fileServerId ?? await waitForValue(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
41842
42969
  let lastError = new Error("Gateway port file unavailable");
41843
42970
  while (Date.now() < timeoutAt) {
41844
42971
  if (child.exitCode !== null || child.signalCode !== null || spawnError() !== undefined) {
@@ -41875,10 +43002,10 @@ async function connectSpawned(plan, child, outputTail, spawnError) {
41875
43002
  }
41876
43003
  let socketPath = plan.socketPath;
41877
43004
  if (socketPath === undefined) {
41878
- const generated = await waitForValue2(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
43005
+ const generated = await waitForValue(() => readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2)), "gateway serverId file", timeoutMs, child, outputTail, spawnError);
41879
43006
  socketPath = join15(plan.nekoDir, `${generated}.sock`);
41880
43007
  }
41881
- await waitForValue2(() => existsSync3(socketPath) ? true : undefined, `gateway ready (socket: ${socketPath})`, timeoutMs, child, outputTail, spawnError);
43008
+ await waitForValue(() => existsSync3(socketPath) ? true : undefined, `gateway ready (socket: ${socketPath})`, timeoutMs, child, outputTail, spawnError);
41882
43009
  const serverId = plan.fileServerId ?? serverIdFromSocketPath2(socketPath) ?? readServerId(join15(plan.nekoDir, SERVER_ID_FILENAME2));
41883
43010
  if (serverId === undefined) {
41884
43011
  throw new Error(`Cannot determine gateway serverId (socket: ${socketPath})`);
@@ -41975,7 +43102,7 @@ function captureOutput(child) {
41975
43102
  return () => [stdout.trimEnd(), stderr.trimEnd()].filter((part) => part !== "").join(`
41976
43103
  `);
41977
43104
  }
41978
- async function waitForValue2(poll, what, timeoutMs, child, outputTail, spawnError) {
43105
+ async function waitForValue(poll, what, timeoutMs, child, outputTail, spawnError) {
41979
43106
  const start = Date.now();
41980
43107
  for (;; ) {
41981
43108
  const error = spawnError();
@@ -42010,15 +43137,14 @@ var init_gateway2 = __esm(() => {
42010
43137
  });
42011
43138
 
42012
43139
  // packages/tui/src/help-ui.ts
42013
- import { getKeybindings, matchesKey as matchesKey7, TUI_KEYBINDINGS, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
43140
+ import { getKeybindings, matchesKey as matchesKey8, TUI_KEYBINDINGS, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
42014
43141
  function helpLines(platform = process.platform) {
42015
43142
  const rows = [];
42016
43143
  const section = (title, hints) => {
42017
43144
  rows.push(theme.accent(title), ...hints.map(([key, description]) => shortcutHints([key, description])), "");
42018
43145
  };
42019
43146
  section("Chat", [
42020
- ["Enter", "Send; steer while running"],
42021
- ["Alt+Enter", "Queue while running"],
43147
+ ["Enter", "Send; queue while running"],
42022
43148
  ["Esc", "Stop a running task or blocking compaction"],
42023
43149
  ["Esc Esc", "Clear draft when idle (press twice quickly)"],
42024
43150
  ["Up / Down", "Browse previous inputs in this session"],
@@ -42055,6 +43181,21 @@ function helpLines(platform = process.platform) {
42055
43181
  ["End", "Jump to the newest output"],
42056
43182
  ["\u2190", "Back"]
42057
43183
  ]);
43184
+ section("Queue", [
43185
+ ["Alt+\u2191", "Select queued messages above the editor"],
43186
+ ["\u2191 / \u2193", "Move between queued messages"],
43187
+ ["Enter", "Steer the selected message (joins the run after the current tool round)"],
43188
+ ["Esc", "Withdraw the selected message into the editor; the run keeps going"]
43189
+ ]);
43190
+ section("Web search", [
43191
+ ["/search", "Choose the web search provider"],
43192
+ ["Enter", "Apply the highlighted provider"]
43193
+ ]);
43194
+ section("Context", [
43195
+ ["/context", "Tune compaction and tool result pruning"],
43196
+ ["Enter", "Choose a setting, then a value to save"],
43197
+ ["Esc", "Back"]
43198
+ ]);
42058
43199
  section("Skills", [
42059
43200
  ["U", "Install for user"],
42060
43201
  ["P", "Install for project"],
@@ -42107,13 +43248,13 @@ class HelpDetail {
42107
43248
  this.#offset = Math.max(0, Math.min(this.#max, this.#offset + delta));
42108
43249
  }
42109
43250
  handleKey(data) {
42110
- if (matchesKey7(data, "up"))
43251
+ if (matchesKey8(data, "up"))
42111
43252
  this.scroll(-1);
42112
- else if (matchesKey7(data, "down"))
43253
+ else if (matchesKey8(data, "down"))
42113
43254
  this.scroll(1);
42114
- else if (matchesKey7(data, "pageUp"))
43255
+ else if (matchesKey8(data, "pageUp"))
42115
43256
  this.scroll(-this.#page);
42116
- else if (matchesKey7(data, "pageDown"))
43257
+ else if (matchesKey8(data, "pageDown"))
42117
43258
  this.scroll(this.#page);
42118
43259
  else
42119
43260
  return false;
@@ -42140,7 +43281,7 @@ import { readFile as readFile9, stat as stat5 } from "fs/promises";
42140
43281
  import { homedir as homedir8 } from "os";
42141
43282
  import { basename as basename8, resolve as resolve5 } from "path";
42142
43283
  import { promisify as promisify2 } from "util";
42143
- import { isKeyRelease as isKeyRelease4, isKeyRepeat as isKeyRepeat2, matchesKey as matchesKey8 } from "@earendil-works/pi-tui";
43284
+ import { isKeyRelease as isKeyRelease4, isKeyRepeat as isKeyRepeat2, matchesKey as matchesKey9 } from "@earendil-works/pi-tui";
42144
43285
  function imagePaths(text, platform = process.platform) {
42145
43286
  const paths = [];
42146
43287
  let word = "", quote = "", escaped = false;
@@ -42234,7 +43375,7 @@ end try`]).catch(() => ""));
42234
43375
  return { text: String(await run(tool, wayland ? ["--no-newline"] : ["-selection", "clipboard", "-o"])) };
42235
43376
  }
42236
43377
  function isClipboardShortcut(data, platform = process.platform) {
42237
- return !isKeyRelease4(data) && !isKeyRepeat2(data) && matchesKey8(data, platform === "win32" ? "alt+v" : "ctrl+v");
43378
+ return !isKeyRelease4(data) && !isKeyRepeat2(data) && matchesKey9(data, platform === "win32" ? "alt+v" : "ctrl+v");
42238
43379
  }
42239
43380
  var exec;
42240
43381
  var init_image_input = __esm(() => {
@@ -42249,7 +43390,7 @@ function createAppLayout(parts) {
42249
43390
  parts.approval.height = Math.min(10, Math.max(7, Math.floor(v.height / 2)));
42250
43391
  parts.footer.panelOpen = parts.panelOpen();
42251
43392
  parts.editor.maxRows = parts.panelOpen() ? 3 : v.height >= 24 ? 7 : 5;
42252
- parts.panel.height = Math.max(1, v.height - (parts.approvalVisible?.() ? parts.approval?.height ?? 0 : 0) - Math.min(7, Math.max(3, parts.editor.render(v.width).length)) - 2 - Math.min(1, parts.notices.render(v.width).length) - (parts.busyVisible?.() ? Math.min(2, parts.busy?.render(v.width).length ?? 0) : 0));
43393
+ parts.panel.height = Math.max(1, v.height - (parts.approvalVisible?.() ? parts.approval?.height ?? 0 : 0) - Math.min(7, Math.max(3, parts.editor.render(v.width).length)) - 2 - Math.min(1, parts.notices.render(v.width).length) - (parts.busyVisible?.() ? Math.min(2, parts.busy?.render(v.width).length ?? 0) : 0) - (parts.queueVisible?.() ? Math.min(3, parts.queue?.render(v.width).length ?? 0) : 0));
42253
43394
  return usableSize(v.width, v.height);
42254
43395
  };
42255
43396
  return new VStack([
@@ -42267,6 +43408,12 @@ function createAppLayout(parts) {
42267
43408
  shrink: 0,
42268
43409
  visible: (v) => normal(v) && (parts.busyVisible?.() ?? false)
42269
43410
  },
43411
+ {
43412
+ component: parts.queue ?? new Text8("", 0, 0),
43413
+ maxSize: 3,
43414
+ shrink: 0,
43415
+ visible: (v) => normal(v) && (parts.queueVisible?.() ?? false)
43416
+ },
42270
43417
  {
42271
43418
  component: parts.approval ?? new Text8("", 0, 0),
42272
43419
  shrink: 0,
@@ -42279,9 +43426,154 @@ function createAppLayout(parts) {
42279
43426
  var usableSize = (width, height) => width >= 40 && height >= 10;
42280
43427
  var init_layout = () => {};
42281
43428
 
43429
+ // packages/tui/src/queue-ui.ts
43430
+ import { matchesKey as matchesKey10, truncateToWidth as truncateToWidth6 } from "@earendil-works/pi-tui";
43431
+
43432
+ class QueueRow {
43433
+ #items = [];
43434
+ #selected;
43435
+ setItems(items) {
43436
+ this.#items = items;
43437
+ if (items.length === 0)
43438
+ this.#selected = undefined;
43439
+ else if (this.#selected !== undefined)
43440
+ this.#selected = Math.min(this.#selected, items.length - 1);
43441
+ }
43442
+ get visible() {
43443
+ return this.#items.length > 0;
43444
+ }
43445
+ get selecting() {
43446
+ return this.#selected !== undefined;
43447
+ }
43448
+ get current() {
43449
+ return this.#selected === undefined ? undefined : this.#items[this.#selected];
43450
+ }
43451
+ select() {
43452
+ if (this.#items.length === 0)
43453
+ return false;
43454
+ this.#selected = this.#items.length - 1;
43455
+ return true;
43456
+ }
43457
+ clear() {
43458
+ this.#selected = undefined;
43459
+ }
43460
+ move(delta) {
43461
+ if (this.#selected === undefined)
43462
+ return;
43463
+ const next = this.#selected + delta;
43464
+ if (next >= this.#items.length)
43465
+ this.#selected = undefined;
43466
+ else
43467
+ this.#selected = Math.max(0, next);
43468
+ }
43469
+ render(width) {
43470
+ const total = this.#items.length;
43471
+ if (total === 0)
43472
+ return [];
43473
+ const end = this.#selected === undefined ? total : Math.max(this.#selected + 1, Math.min(total, QUEUE_ROWS));
43474
+ const start = Math.max(0, end - QUEUE_ROWS);
43475
+ const rows = this.#items.slice(start, end).map((item, offset) => {
43476
+ const picked = start + offset === this.#selected;
43477
+ const text = `${modeLabel(item.mode)} \xB7 ${content2(item)}`;
43478
+ return truncateToWidth6(picked ? ` ${theme.accent(`\u203A ${text}`)}` : ` ${theme.muted(text)}`, width);
43479
+ });
43480
+ const earlier = start > 0 ? `${start} earlier \xB7 ` : "";
43481
+ const later = end < total ? `${total - end} newer \xB7 ` : "";
43482
+ const hint = this.#selected === undefined ? shortcutHints(["Alt+\u2191", "Select queued"]) : shortcutHints(["Enter", "Steer"], ["Esc", "Withdraw"], ["\u2191/\u2193", "Move"]);
43483
+ rows.push(truncateToWidth6(theme.dim(` ${earlier}${later}${hint}`), width));
43484
+ return rows;
43485
+ }
43486
+ invalidate() {}
43487
+ }
43488
+ function queueKeyAction(row, data, autocompleteOpen) {
43489
+ if (!row.selecting)
43490
+ return matchesKey10(data, "alt+up") && row.visible && !autocompleteOpen ? "select" : undefined;
43491
+ if (matchesKey10(data, "up"))
43492
+ return "up";
43493
+ if (matchesKey10(data, "down"))
43494
+ return "down";
43495
+ if (matchesKey10(data, "enter"))
43496
+ return "steer";
43497
+ if (matchesKey10(data, "escape"))
43498
+ return "withdraw";
43499
+ return "leave";
43500
+ }
43501
+ function restoreText(existing, text) {
43502
+ return existing.trim() ? `${existing}
43503
+ ${text}` : text;
43504
+ }
43505
+ function steerNotice(outcome) {
43506
+ if (outcome.result === "steered")
43507
+ return { message: "Message will steer the current run.", level: "info" };
43508
+ if (outcome.result === "withdrawn")
43509
+ return { message: "Could not steer; the message was withdrawn into the editor.", level: "warning" };
43510
+ if (outcome.result === "unknown")
43511
+ return { message: "That message is no longer queued.", level: "warning" };
43512
+ return { message: "That message was already sent and cannot be steered.", level: "warning" };
43513
+ }
43514
+ function withdrawNotice(outcome) {
43515
+ if (outcome.result === "withdrawn")
43516
+ return outcome.images ? { message: "Message withdrawn into the editor. Images are not restored; attach them again.", level: "warning" } : { message: "Message withdrawn into the editor.", level: "info" };
43517
+ if (outcome.result === "unknown")
43518
+ return { message: "That message is no longer queued.", level: "warning" };
43519
+ return { message: "That message was already sent and cannot be withdrawn.", level: "warning" };
43520
+ }
43521
+ var QUEUE_ROWS = 2, QUEUE_BLOCK_LINES, modeLabel = (mode) => mode === "steer" ? "Steer" : "Queued", oneLine3 = (text) => text.replace(/\s+/g, " ").trim(), content2 = (item) => oneLine3(item.preview) || (item.images > 0 ? `[${item.images} image${item.images > 1 ? "s" : ""}]` : "");
43522
+ var init_queue_ui = __esm(() => {
43523
+ init_shortcut_hints();
43524
+ init_theme();
43525
+ QUEUE_BLOCK_LINES = QUEUE_ROWS + 1;
43526
+ });
43527
+
43528
+ // packages/tui/src/search-ui.ts
43529
+ function searchItems(settings, credentials) {
43530
+ if (!settings)
43531
+ return [];
43532
+ const source = (provider) => {
43533
+ const status = credentials?.statuses[provider];
43534
+ return status ? SOURCES2[status.source] : "loading";
43535
+ };
43536
+ const deepseekMissing = credentials?.statuses.deepseek.source === "none";
43537
+ const descriptions = {
43538
+ tavily: `Tavily search (${source("tavily")}). Default`,
43539
+ deepseek: deepseekMissing ? "No DeepSeek key. Run /login to set one before choosing it" : `DeepSeek search and fetch with your DeepSeek API key (${source("deepseek")})`
43540
+ };
43541
+ return SEARCH_PROVIDERS.map((value) => ({
43542
+ value,
43543
+ label: `${NAMES[value]}${settings.selected === value ? " (Current)" : ""}`,
43544
+ description: descriptions[value]
43545
+ }));
43546
+ }
43547
+ function openSearchPanel(panel, client, notify) {
43548
+ const items = () => searchItems(client.searchSettings.state.value, client.credentials.state.value);
43549
+ const refresh = () => panel.update(items());
43550
+ const stops = [client.searchSettings.state.subscribe(refresh), client.credentials.state.subscribe(refresh)];
43551
+ panel.emptyText = SEARCH_EMPTY;
43552
+ panel.show("Web search", async () => items(), async (value) => {
43553
+ await client.searchSettings.set({ provider: value });
43554
+ notify(`Web search provider set to ${NAMES[value]}.`);
43555
+ });
43556
+ return () => {
43557
+ for (const stop of stops)
43558
+ stop();
43559
+ };
43560
+ }
43561
+ var SEARCH_EMPTY = "Search settings are unavailable.", NAMES, SOURCES2;
43562
+ var init_search_ui = __esm(() => {
43563
+ init_search_providers();
43564
+ NAMES = { tavily: "Tavily", deepseek: "DeepSeek" };
43565
+ SOURCES2 = {
43566
+ saved: "saved key",
43567
+ environment: "environment variable",
43568
+ runtime: "runtime key",
43569
+ keyless: "keyless, rate-limited",
43570
+ none: "no key"
43571
+ };
43572
+ });
43573
+
42282
43574
  // packages/tui/src/skill-market-ui.ts
42283
43575
  import { ChordError as ChordError6 } from "@earendil-works/chord";
42284
- import { Markdown as Markdown3, matchesKey as matchesKey9, wrapTextWithAnsi as wrapTextWithAnsi5 } from "@earendil-works/pi-tui";
43576
+ import { Markdown as Markdown3, matchesKey as matchesKey11, wrapTextWithAnsi as wrapTextWithAnsi5 } from "@earendil-works/pi-tui";
42285
43577
 
42286
43578
  class SkillDetailView {
42287
43579
  #skill = null;
@@ -42320,11 +43612,11 @@ class SkillDetailView {
42320
43612
  }
42321
43613
  handleKey(data) {
42322
43614
  if (this.#confirm) {
42323
- if (matchesKey9(data, "y")) {
43615
+ if (matchesKey11(data, "y")) {
42324
43616
  const pending = this.#confirm;
42325
43617
  this.#confirm = null;
42326
43618
  this.run(pending.run);
42327
- } else if (matchesKey9(data, "n")) {
43619
+ } else if (matchesKey11(data, "n")) {
42328
43620
  this.#confirm = null;
42329
43621
  }
42330
43622
  return true;
@@ -42333,17 +43625,17 @@ class SkillDetailView {
42333
43625
  return true;
42334
43626
  const skill = this.#skill;
42335
43627
  const builtinOnly = skill.installed.builtin && !skill.installed.user && !skill.installed.project;
42336
- if (matchesKey9(data, "v")) {
43628
+ if (matchesKey11(data, "v")) {
42337
43629
  this.#openPreview(this.#name);
42338
43630
  return true;
42339
43631
  }
42340
43632
  if (builtinOnly)
42341
43633
  return true;
42342
- if (matchesKey9(data, "u"))
43634
+ if (matchesKey11(data, "u"))
42343
43635
  this.askInstall("user", skill);
42344
- else if (matchesKey9(data, "p"))
43636
+ else if (matchesKey11(data, "p"))
42345
43637
  this.askInstall("project", skill);
42346
- else if (matchesKey9(data, "d"))
43638
+ else if (matchesKey11(data, "d"))
42347
43639
  this.askUninstall(skill);
42348
43640
  else
42349
43641
  return false;
@@ -42468,13 +43760,13 @@ class SkillPreviewView {
42468
43760
  this.#offset = Math.max(0, Math.min(this.#maxOffset, this.#offset + delta));
42469
43761
  }
42470
43762
  handleKey(data) {
42471
- if (matchesKey9(data, "up"))
43763
+ if (matchesKey11(data, "up"))
42472
43764
  this.scroll(-1);
42473
- else if (matchesKey9(data, "down"))
43765
+ else if (matchesKey11(data, "down"))
42474
43766
  this.scroll(1);
42475
- else if (matchesKey9(data, "pageUp"))
43767
+ else if (matchesKey11(data, "pageUp"))
42476
43768
  this.scroll(-this.#pageSize);
42477
- else if (matchesKey9(data, "pageDown"))
43769
+ else if (matchesKey11(data, "pageDown"))
42478
43770
  this.scroll(this.#pageSize);
42479
43771
  else
42480
43772
  return false;
@@ -42526,7 +43818,7 @@ var init_skill_market_ui = __esm(() => {
42526
43818
  });
42527
43819
 
42528
43820
  // packages/tui/src/subagents-ui.ts
42529
- import { matchesKey as matchesKey10 } from "@earendil-works/pi-tui";
43821
+ import { matchesKey as matchesKey12 } from "@earendil-works/pi-tui";
42530
43822
 
42531
43823
  class SubagentDetail {
42532
43824
  get help() {
@@ -42646,13 +43938,13 @@ class SubagentDetail {
42646
43938
  this.history();
42647
43939
  }
42648
43940
  handleKey(data) {
42649
- if (matchesKey10(data, "up")) {
43941
+ if (matchesKey12(data, "up")) {
42650
43942
  this.scroll(-1);
42651
- } else if (matchesKey10(data, "down")) {
43943
+ } else if (matchesKey12(data, "down")) {
42652
43944
  this.scroll(1);
42653
- } else if (matchesKey10(data, "pageUp")) {
43945
+ } else if (matchesKey12(data, "pageUp")) {
42654
43946
  this.scroll(-this.#pageSize);
42655
- } else if (matchesKey10(data, "pageDown")) {
43947
+ } else if (matchesKey12(data, "pageDown")) {
42656
43948
  this.scroll(this.#pageSize);
42657
43949
  } else if (data.toLowerCase() === "e") {
42658
43950
  this.#expanded = !this.#expanded;
@@ -42668,7 +43960,7 @@ class SubagentDetail {
42668
43960
  help: shortcutHints(["Enter", "Stop"], ["Esc", "Back"]),
42669
43961
  lines: () => [theme.warning("Stop this task? Existing changes will be kept.")],
42670
43962
  handleKey: (key) => {
42671
- if (!matchesKey10(key, "enter"))
43963
+ if (!matchesKey12(key, "enter"))
42672
43964
  return false;
42673
43965
  this.#service.stop(stopTaskId === undefined ? null : { taskId: stopTaskId }).catch((e) => {
42674
43966
  this.#error = String(e);
@@ -42799,7 +44091,7 @@ var init_subagents_ui = __esm(() => {
42799
44091
  });
42800
44092
 
42801
44093
  // packages/tui/src/version-ui.ts
42802
- import { matchesKey as matchesKey11, Text as Text9 } from "@earendil-works/pi-tui";
44094
+ import { matchesKey as matchesKey13, Text as Text9 } from "@earendil-works/pi-tui";
42803
44095
  function actionFor(state, installed, runningVersion) {
42804
44096
  const action = updateAction({ gateway: state, frontend: runningVersion, installed });
42805
44097
  return action === "install" && !newer(state?.latestVersion, runningVersion) ? "none" : action;
@@ -42877,9 +44169,9 @@ class VersionDetail {
42877
44169
  return lines.slice(this.#offset, this.#offset + height);
42878
44170
  }
42879
44171
  handleKey(data) {
42880
- if (matchesKey11(data, "up"))
44172
+ if (matchesKey13(data, "up"))
42881
44173
  this.#offset = Math.max(0, this.#offset - 1);
42882
- else if (matchesKey11(data, "down"))
44174
+ else if (matchesKey13(data, "down"))
42883
44175
  this.#offset++;
42884
44176
  else
42885
44177
  return false;
@@ -42944,7 +44236,7 @@ import {
42944
44236
  Container as Container6,
42945
44237
  isKeyRelease as isKeyRelease5,
42946
44238
  Loader,
42947
- matchesKey as matchesKey12,
44239
+ matchesKey as matchesKey14,
42948
44240
  ProcessTerminal,
42949
44241
  ScrollView,
42950
44242
  TuiAltScreen
@@ -42980,6 +44272,10 @@ class NekoTuiApp {
42980
44272
  #chat;
42981
44273
  #agentsPanel = false;
42982
44274
  #jobsPanel = false;
44275
+ #queueBusy = false;
44276
+ #queueRow = new QueueRow;
44277
+ #searchStop;
44278
+ #contextStop;
42983
44279
  #agentDetails = new Map;
42984
44280
  #sessionId = "";
42985
44281
  #viewUnsubscribe;
@@ -43021,6 +44317,10 @@ class NekoTuiApp {
43021
44317
  this.#thinkingModel = undefined;
43022
44318
  this.#agentsPanel = false;
43023
44319
  this.#jobsPanel = false;
44320
+ this.#searchStop?.();
44321
+ this.#searchStop = undefined;
44322
+ this.#contextStop?.();
44323
+ this.#contextStop = undefined;
43024
44324
  clearInterval(this.#sessionPanelTimer);
43025
44325
  this.#sessionPanelTimer = undefined;
43026
44326
  this.#ui.setFocus(this.#editor);
@@ -43081,10 +44381,10 @@ class NekoTuiApp {
43081
44381
  this.#ui.addInputListener((data) => {
43082
44382
  if (isKeyRelease5(data))
43083
44383
  return { consume: true };
43084
- if (!matchesKey12(data, "escape") || this.#panel.open || this.#login.open || this.#busyText !== undefined || this.#lastView?.compaction?.active.length)
44384
+ if (!matchesKey14(data, "escape") || this.#panel.open || this.#login.open || this.#busyText !== undefined || this.#lastView?.compaction?.active.length)
43085
44385
  this.#editor.resetEscapeSequence();
43086
44386
  if (!usableSize(this.#terminal.columns, this.#terminal.rows)) {
43087
- if (matchesKey12(data, "ctrl+c"))
44387
+ if (matchesKey14(data, "ctrl+c"))
43088
44388
  this.exit();
43089
44389
  return { consume: true };
43090
44390
  }
@@ -43095,11 +44395,14 @@ class NekoTuiApp {
43095
44395
  }
43096
44396
  if (this.#sessionOperation)
43097
44397
  return { consume: true };
43098
- if (matchesKey12(data, "right") && this.#lastView?.subagents?.length && this.#editor.getText() === "" && !this.#editor.isShowingAutocomplete()) {
44398
+ const queueKey = this.#queueKey(data);
44399
+ if (queueKey)
44400
+ return queueKey;
44401
+ if (matchesKey14(data, "right") && this.#lastView?.subagents?.length && this.#editor.getText() === "" && !this.#editor.isShowingAutocomplete()) {
43099
44402
  this.#showAgents();
43100
44403
  return { consume: true };
43101
44404
  }
43102
- if (matchesKey12(data, process.platform === "win32" ? "alt+v" : "ctrl+v")) {
44405
+ if (matchesKey14(data, process.platform === "win32" ? "alt+v" : "ctrl+v")) {
43103
44406
  if (isClipboardShortcut(data))
43104
44407
  this.pasteClipboard();
43105
44408
  return { consume: true };
@@ -43116,29 +44419,25 @@ class NekoTuiApp {
43116
44419
  return { consume: true };
43117
44420
  }
43118
44421
  }
43119
- if (matchesKey12(data, "escape") && this.#editor.isShowingAutocomplete()) {
44422
+ if (matchesKey14(data, "escape") && this.#editor.isShowingAutocomplete()) {
43120
44423
  this.#editor.resetEscapeSequence();
43121
44424
  return;
43122
44425
  }
43123
- if (matchesKey12(data, "ctrl+c")) {
44426
+ if (matchesKey14(data, "ctrl+c")) {
43124
44427
  if (this.#busyText !== undefined)
43125
44428
  this.abort();
43126
44429
  else
43127
44430
  this.exit();
43128
44431
  return { consume: true };
43129
44432
  }
43130
- if (matchesKey12(data, "escape") && (this.#busyText !== undefined || this.#lastView?.compaction?.active.some((c) => c.reason === "manual" || c.blocking))) {
44433
+ if (matchesKey14(data, "escape") && (this.#busyText !== undefined || this.#lastView?.compaction?.active.some((c) => c.reason === "manual" || c.blocking))) {
43131
44434
  this.abort();
43132
44435
  return { consume: true };
43133
44436
  }
43134
- if (matchesKey12(data, "ctrl+s")) {
44437
+ if (matchesKey14(data, "ctrl+s")) {
43135
44438
  this.openSessionsOverlay();
43136
44439
  return { consume: true };
43137
44440
  }
43138
- if (this.#busyText !== undefined && matchesKey12(data, "alt+enter")) {
43139
- this.submitFollowUp();
43140
- return { consume: true };
43141
- }
43142
44441
  return;
43143
44442
  });
43144
44443
  }
@@ -43182,6 +44481,8 @@ class NekoTuiApp {
43182
44481
  notices,
43183
44482
  busy: this.#busyLoader,
43184
44483
  busyVisible: () => this.#busyText !== undefined,
44484
+ queue: this.#queueRow,
44485
+ queueVisible: () => this.#queueRow.visible,
43185
44486
  panelOpen: () => this.#panel.open || this.#login.open
43186
44487
  }));
43187
44488
  this.#ui.setFocus(this.#editor);
@@ -43194,7 +44495,7 @@ class NekoTuiApp {
43194
44495
  const installed = await installedVersion();
43195
44496
  if (this.#exitStarted)
43196
44497
  return;
43197
- const notice = versionNotice(this.#gateway.client.application.info.value ?? null, installed);
44498
+ const notice = versionNotice(this.#gateway.client.application.info.value, installed);
43198
44499
  this.#versionNotice.clear();
43199
44500
  if (notice)
43200
44501
  this.#versionNotice.addChild(new ThemedTruncatedText(() => formatNotice(notice, "info"), 1, 0));
@@ -43242,6 +44543,14 @@ class NekoTuiApp {
43242
44543
  this.#showJobs();
43243
44544
  return;
43244
44545
  }
44546
+ if (trimmed === "/search") {
44547
+ this.#showSearch();
44548
+ return;
44549
+ }
44550
+ if (trimmed === "/context") {
44551
+ this.#showContext();
44552
+ return;
44553
+ }
43245
44554
  const instructions = compactInstructions(trimmed);
43246
44555
  if (instructions !== null) {
43247
44556
  const chat = this.#chat;
@@ -43293,7 +44602,7 @@ class NekoTuiApp {
43293
44602
  if (trimmed === "/version") {
43294
44603
  if (!this.#exitStarted && !this.#panel.open && !this.#login.open) {
43295
44604
  openVersion(this.#panel, async () => ({
43296
- state: this.#gateway.client.application.info.value ?? null,
44605
+ state: this.#gateway.client.application.info.value,
43297
44606
  installed: await installedVersion()
43298
44607
  }), () => this.#gateway.client.application.check(null));
43299
44608
  this.#ui.setFocus(this.#panel);
@@ -43327,17 +44636,12 @@ class NekoTuiApp {
43327
44636
  const chat = this.#chat;
43328
44637
  if (chat === undefined)
43329
44638
  return;
43330
- if (this.#lastView?.agent.model?.provider === "deepseek" && this.#gateway.client.credentials.state.value?.statuses.deepseek.source === "none") {
44639
+ if (this.#lastView?.agent.model?.provider === "deepseek" && this.#gateway.client.credentials.state.value.statuses.deepseek.source === "none") {
43331
44640
  this.#editor.setText(text);
43332
44641
  this.addNotice("Configure DeepSeek with /login before sending.", "warning");
43333
44642
  return;
43334
44643
  }
43335
- this.sendImages(text, this.#busyText !== undefined ? "steer" : undefined);
43336
- }
43337
- submitFollowUp() {
43338
- const text = this.#editor.getText();
43339
- if (text.trim())
43340
- this.sendImages(text, "followUp");
44644
+ this.sendImages(text);
43341
44645
  }
43342
44646
  async addImagePaths(paths) {
43343
44647
  return paths.map((path) => this.addImage(readImageFile(path, this.#options.cwd ?? process.cwd()), path.split(/[\\/]/).at(-1) ?? "Image")).join(" ");
@@ -43372,7 +44676,7 @@ class NekoTuiApp {
43372
44676
  this.#clipboardPending = false;
43373
44677
  }
43374
44678
  }
43375
- async sendImages(text, whenBusy) {
44679
+ async sendImages(text) {
43376
44680
  const chat = this.#chat;
43377
44681
  const session = this.#sessionId;
43378
44682
  if (!chat || this.#sending.has(session))
@@ -43404,7 +44708,7 @@ class NekoTuiApp {
43404
44708
  }));
43405
44709
  content = content.replaceAll(ref, "");
43406
44710
  }
43407
- await chat.submit({ content: content.trim(), images: ids, requestId, ...whenBusy ? { whenBusy } : {} });
44711
+ await chat.submit({ content: content.trim(), images: ids, requestId });
43408
44712
  if (session === this.#sessionId && this.#editor.getText() === before)
43409
44713
  this.#editor.setText("");
43410
44714
  if (this.#texts.get(session) === before)
@@ -43445,7 +44749,7 @@ class NekoTuiApp {
43445
44749
  this.addNotice(messageOf(error), "warning", { wrap: true });
43446
44750
  return;
43447
44751
  }
43448
- const summary = this.#gateway.client.directory.state.value?.sessions.find((s) => s.id === sessionId);
44752
+ const summary = this.#gateway.client.directory.state.value.sessions.find((s) => s.id === sessionId);
43449
44753
  const title = summary ? sessionName(summary, "") : "";
43450
44754
  this.addNotice("Exporting...");
43451
44755
  try {
@@ -43480,6 +44784,7 @@ class NekoTuiApp {
43480
44784
  async switchSession(id) {
43481
44785
  this.#approval.close();
43482
44786
  this.#chat = undefined;
44787
+ this.#queueRow.setItems([]);
43483
44788
  if (this.#jobsPanel && this.#panel.open)
43484
44789
  this.#panel.close();
43485
44790
  if (this.#sessionId)
@@ -43498,8 +44803,7 @@ class NekoTuiApp {
43498
44803
  this.#historyItems = undefined;
43499
44804
  this.#resetTranscript();
43500
44805
  const view = handle.view;
43501
- if (view.value !== undefined)
43502
- this.applyView(view.value);
44806
+ this.applyView(view.value);
43503
44807
  this.#viewUnsubscribe = view.subscribe(() => {
43504
44808
  this.scheduleApply();
43505
44809
  });
@@ -43541,7 +44845,7 @@ class NekoTuiApp {
43541
44845
  async archiveSession() {
43542
44846
  const current = this.#sessionId;
43543
44847
  await this.#gateway.client.management.archive(current);
43544
- const next = recentlyVisited(this.#gateway.client.directory.state.value?.sessions ?? [], this.#options.cwd).find((s) => s.id !== current);
44848
+ const next = recentlyVisited(this.#gateway.client.directory.state.value.sessions, this.#options.cwd).find((s) => s.id !== current);
43545
44849
  if (next)
43546
44850
  await this.switchSession(next.id);
43547
44851
  else
@@ -43550,7 +44854,7 @@ class NekoTuiApp {
43550
44854
  sessionItems() {
43551
44855
  return [
43552
44856
  { value: NEW_SESSION_VALUE, label: "+ New session", description: this.#options.cwd },
43553
- ...sessionPickerItems(this.#gateway.client.directory.state.value?.sessions ?? [], this.#options.cwd, this.#sessionId)
44857
+ ...sessionPickerItems(this.#gateway.client.directory.state.value.sessions, this.#options.cwd, this.#sessionId)
43554
44858
  ];
43555
44859
  }
43556
44860
  openSessionsOverlay() {
@@ -43685,7 +44989,7 @@ class NekoTuiApp {
43685
44989
  this.#panel.pointerSelection = true;
43686
44990
  this.#panel.emptyText = JOBS_EMPTY;
43687
44991
  this.#panel.listKey = (data, value) => {
43688
- if (matchesKey12(data, "left")) {
44992
+ if (matchesKey14(data, "left")) {
43689
44993
  this.#panel.close();
43690
44994
  return true;
43691
44995
  }
@@ -43699,6 +45003,76 @@ class NekoTuiApp {
43699
45003
  this.#panel.show("Jobs", async () => jobItems(jobs.list()), async (value) => new JobDetail(jobs, value, this.#panel, changed).open());
43700
45004
  this.#ui.setFocus(this.#panel);
43701
45005
  }
45006
+ #queueKey(data) {
45007
+ const row = this.#queueRow;
45008
+ if (this.#queueBusy && row.selecting)
45009
+ return { consume: true };
45010
+ const action = queueKeyAction(row, data, this.#editor.isShowingAutocomplete());
45011
+ if (action === undefined)
45012
+ return;
45013
+ if (action === "steer" || action === "withdraw")
45014
+ return this.#queueAct(action);
45015
+ if (action === "select")
45016
+ row.select();
45017
+ else if (action === "up")
45018
+ row.move(-1);
45019
+ else if (action === "down")
45020
+ row.move(1);
45021
+ else {
45022
+ row.clear();
45023
+ this.#ui.requestRender();
45024
+ return;
45025
+ }
45026
+ this.#ui.requestRender();
45027
+ return { consume: true };
45028
+ }
45029
+ #queueAct(action) {
45030
+ const chat = this.#chat;
45031
+ const item = this.#queueRow.current;
45032
+ if (!chat || !item)
45033
+ return { consume: true };
45034
+ if (action === "steer" && item.mode === "steer") {
45035
+ this.addNotice("That message already steers the current run.", "info");
45036
+ return { consume: true };
45037
+ }
45038
+ const queue = queueClient(chat);
45039
+ const session = this.#sessionId;
45040
+ this.#queueBusy = true;
45041
+ const restore = (outcome) => {
45042
+ if (outcome.result === "withdrawn")
45043
+ this.#editor.setText(restoreText(this.#editor.getText(), outcome.text ?? ""));
45044
+ };
45045
+ const run = action === "steer" ? queue.steer(item.id).then((outcome) => {
45046
+ if (session !== this.#sessionId)
45047
+ return;
45048
+ restore(outcome);
45049
+ const notice = steerNotice(outcome);
45050
+ this.addNotice(notice.message, notice.level);
45051
+ }) : queue.withdraw(item.id).then((outcome) => {
45052
+ if (session !== this.#sessionId)
45053
+ return;
45054
+ restore(outcome);
45055
+ const notice = withdrawNotice(outcome);
45056
+ this.addNotice(notice.message, notice.level);
45057
+ });
45058
+ run.catch((error) => this.addNotice(messageOf(error), "error")).finally(() => {
45059
+ this.#queueBusy = false;
45060
+ this.#ui.requestRender();
45061
+ });
45062
+ return { consume: true };
45063
+ }
45064
+ #showSearch() {
45065
+ if (this.#exitStarted || this.#panel.open || this.#login.open)
45066
+ return;
45067
+ this.#searchStop = openSearchPanel(this.#panel, this.#gateway.client, (message) => this.addNotice(message));
45068
+ this.#ui.setFocus(this.#panel);
45069
+ }
45070
+ #showContext() {
45071
+ if (this.#exitStarted || this.#panel.open || this.#login.open)
45072
+ return;
45073
+ this.#contextStop = openContextPanel(this.#panel, this.#gateway.client, (message) => this.addNotice(message));
45074
+ this.#ui.setFocus(this.#panel);
45075
+ }
43702
45076
  #agentDetail(target) {
43703
45077
  const round = typeof target === "number" ? this.#lastView?.subagents?.find((r) => r.taskId === target) : undefined;
43704
45078
  const id = typeof target === "string" ? target : round?.agentId ?? `agent-${round?.childId}`;
@@ -43765,6 +45139,7 @@ class NekoTuiApp {
43765
45139
  this.#panel.update(subagentItems(view.subagents ?? [], view.agents));
43766
45140
  if (this.#jobsPanel)
43767
45141
  this.#panel.update(jobItems(view.jobs ?? []));
45142
+ this.#queueRow.setItems(view.queued ?? []);
43768
45143
  for (const identity of view.agents ?? []) {
43769
45144
  const detail = this.#agentDetails.get(identity.agentId);
43770
45145
  if (detail) {
@@ -44085,14 +45460,7 @@ async function runTui(options = {}) {
44085
45460
  }
44086
45461
  }
44087
45462
  async function initialSessionId(gateway, cwd) {
44088
- const state = gateway.client.directory.state;
44089
- const start = Date.now();
44090
- while (state.value === undefined) {
44091
- if (Date.now() - start > 5000)
44092
- throw new Error("Timed out loading sessions");
44093
- await new Promise((resolve) => setTimeout(resolve, 25));
44094
- }
44095
- const sessions = state.value?.sessions ?? [];
45463
+ const sessions = gateway.client.directory.state.value.sessions;
44096
45464
  const first = recentlyVisited(sessions, cwd)[0];
44097
45465
  if (first !== undefined)
44098
45466
  return first.id;
@@ -44118,6 +45486,7 @@ var init_tui = __esm(() => {
44118
45486
  init_tool_execution();
44119
45487
  init_user_message();
44120
45488
  init_welcome();
45489
+ init_context_ui();
44121
45490
  init_export_command();
44122
45491
  init_file_completion();
44123
45492
  init_gateway2();
@@ -44126,6 +45495,8 @@ var init_tui = __esm(() => {
44126
45495
  init_jobs_ui();
44127
45496
  init_labels();
44128
45497
  init_layout();
45498
+ init_queue_ui();
45499
+ init_search_ui();
44129
45500
  init_skill_market_ui();
44130
45501
  init_subagents_ui();
44131
45502
  init_theme();
@@ -44216,7 +45587,7 @@ var USAGE = `\u7528\u6CD5: neko tui [\u9009\u9879]
44216
45587
 
44217
45588
  \u6062\u590D\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6700\u8FD1\u4F7F\u7528\u7684\u4F1A\u8BDD\uFF0C\u4E0D\u5B58\u5728\u5219\u521B\u5EFA\u3002
44218
45589
  \u6309\u9700\u62C9\u8D77\u7F51\u5173\uFF0C\u5C5E\u4E3B TUI \u9000\u51FA\u65F6\u968F\u4E4B\u505C\u6B62\u3002
44219
- \u53EF\u7528 /login \u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
45590
+ \u53EF\u7528 /login \u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\uFF1B/search \u9009\u62E9\u8054\u7F51\u641C\u7D22\u670D\u52A1\uFF08tavily / deepseek\uFF0C\u9ED8\u8BA4 tavily\uFF09\uFF1B/context \u8C03\u6574\u4E0A\u4E0B\u6587\u538B\u7F29\u4E0E\u5DE5\u5177\u7ED3\u679C\u88C1\u526A\u3002`;
44220
45591
  var init_main = __esm(async () => {
44221
45592
  init_tui();
44222
45593
  try {
@@ -44453,7 +45824,7 @@ var init_gateway_spawn = __esm(() => {
44453
45824
 
44454
45825
  // packages/web/src/mobile-admin.ts
44455
45826
  import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual3 } from "crypto";
44456
- import { ChordError as ChordError7, createHost as createHost4 } from "@earendil-works/chord";
45827
+ import { ChordError as ChordError7 } from "@earendil-works/chord";
44457
45828
  function createMobileAdminCaller(gateway, nekoDir, timeouts = {}) {
44458
45829
  const serverId = gateway.serverId;
44459
45830
  let tokenPromise;
@@ -44468,7 +45839,7 @@ function createMobileAdminCaller(gateway, nekoDir, timeouts = {}) {
44468
45839
  const token = await readToken();
44469
45840
  signal.throwIfAborted();
44470
45841
  const link = gateway.socketPath !== undefined ? await createUnixLink(gateway.socketPath) : createWsLink(gateway.wsUrl);
44471
- const host = createHost4({ kind: "client", upstream: identifiedLink(link, serverId), onError: () => {} });
45842
+ const host = createClientHost(link);
44472
45843
  const release = () => {
44473
45844
  host.close().catch(() => {
44474
45845
  return;
@@ -44477,7 +45848,8 @@ function createMobileAdminCaller(gateway, nekoDir, timeouts = {}) {
44477
45848
  };
44478
45849
  signal.addEventListener("abort", release, { once: true });
44479
45850
  try {
44480
- const admin = host.get(NekoMobileAdmin, { token }, { at: serverId });
45851
+ await assertGatewayIdentity(host, serverId, signal);
45852
+ const admin = await host.get(NekoMobileAdmin, { token }, { signal });
44481
45853
  const call = admin[method];
44482
45854
  if (call === undefined)
44483
45855
  throw new Error(`unknown mobile admin method: ${method}`);
@@ -45167,7 +46539,8 @@ var GATEWAY_USAGE = `\u7528\u6CD5: neko serve [\u9009\u9879]
45167
46539
  --config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
45168
46540
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
45169
46541
 
45170
- \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
46542
+ \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY / TAVILY_API_KEY \u914D\u7F6E\u51ED\u636E\uFF1B
46543
+ \u8054\u7F51\u641C\u7D22\u670D\u52A1\u53EF\u5728\u524D\u7AEF\u8BBE\u7F6E\uFF0C\u6216\u7528\u914D\u7F6E\u6587\u4EF6 webSearch.provider\uFF08tavily / deepseek\uFF0C\u9ED8\u8BA4 tavily\uFF09\u3002`;
45171
46544
 
45172
46545
  // packages/gateway/src/main.ts
45173
46546
  var argv, shutdown2, options, gateway;
@@ -45211,7 +46584,8 @@ var USAGE3 = `\u7528\u6CD5: neko <\u547D\u4EE4> [\u9009\u9879]
45211
46584
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
45212
46585
 
45213
46586
  \u5404\u547D\u4EE4\u7684\u9009\u9879\u8BE6\u89C1 neko <\u547D\u4EE4> --help\u3002
45214
- \u53EF\u901A\u8FC7 TUI /login\u3001Web \u8BBE\u7F6E\u9875\u6216 DEEPSEEK_API_KEY / TAVILY_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
46587
+ \u53EF\u901A\u8FC7 TUI /login\u3001Web \u8BBE\u7F6E\u9875\u6216 DEEPSEEK_API_KEY / TAVILY_API_KEY \u914D\u7F6E\u51ED\u636E\u3002
46588
+ \u8054\u7F51\u641C\u7D22\u670D\u52A1\uFF08\u9ED8\u8BA4 Tavily\uFF0C\u53EF\u6539\u9009 DeepSeek\uFF09\u53EF\u7528 TUI /search\u3001Web \u8BBE\u7F6E\u9875\u6216\u914D\u7F6E\u6587\u4EF6 webSearch.provider \u9009\u62E9\u3002`;
45215
46589
  var [command2, ...rest] = process.argv.slice(2);
45216
46590
  switch (command2) {
45217
46591
  case "update": {