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