@gavin-hu/my-pi-agent 0.9.0 → 0.11.0
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 +28 -2
- package/README.zh-CN.md +27 -2
- package/assets/bytefly.schema.json +17 -1
- package/assets/chrome.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
- package/extensions/bytefly/README.md +261 -36
- package/extensions/bytefly/block/component.ts +282 -0
- package/extensions/bytefly/block/config.ts +24 -0
- package/extensions/bytefly/block/fallback.ts +120 -0
- package/extensions/bytefly/block/register.ts +164 -0
- package/extensions/bytefly/chrome.ts +97 -0
- package/extensions/bytefly/doctor/tools.ts +5 -2
- package/extensions/bytefly/header/component.ts +21 -10
- package/extensions/bytefly/header/config.ts +0 -3
- package/extensions/bytefly/header/register.ts +6 -1
- package/extensions/bytefly/index.ts +32 -12
- package/extensions/bytefly/message/config.ts +15 -0
- package/extensions/bytefly/message/format.ts +42 -0
- package/extensions/bytefly/message/register.ts +15 -0
- package/extensions/bytefly/notice/component.ts +107 -0
- package/extensions/bytefly/notice/config.ts +41 -0
- package/extensions/bytefly/notice/register.ts +27 -0
- package/extensions/bytefly/pad.ts +42 -0
- package/extensions/bytefly/separator/component.ts +2 -1
- package/extensions/bytefly/separator/format.ts +17 -10
- package/extensions/bytefly/separator/register.ts +3 -2
- package/extensions/bytefly/working/config.ts +20 -0
- package/extensions/bytefly/working/format.ts +25 -0
- package/extensions/bytefly/working/register.ts +88 -0
- package/extensions/document/README.md +11 -4
- package/extensions/document/paging.ts +10 -10
- package/extensions/document/render.ts +7 -7
- package/extensions/file-browse/README.md +3 -1
- package/extensions/file-browse/client.ts +9 -2
- package/extensions/file-browse/git.ts +4 -7
- package/extensions/file-browse/html.ts +27 -17
- package/extensions/file-open/README.md +6 -0
- package/extensions/goal/README.md +16 -10
- package/extensions/goal/format.ts +7 -20
- package/extensions/goal/tools.ts +5 -17
- package/extensions/goal/tui.ts +12 -40
- package/extensions/job/README.md +113 -35
- package/extensions/job/commands.ts +1 -0
- package/extensions/job/config.ts +4 -0
- package/extensions/job/format.ts +19 -7
- package/extensions/job/index.ts +25 -20
- package/extensions/job/process.ts +24 -2
- package/extensions/job/registry.ts +3 -0
- package/extensions/job/runtime.ts +253 -18
- package/extensions/job/schema.ts +34 -0
- package/extensions/job/store.ts +13 -1
- package/extensions/job/tools.ts +55 -15
- package/extensions/job/tui.ts +41 -15
- package/extensions/job/types.ts +9 -1
- package/extensions/memory/README.md +6 -0
- package/extensions/memory/format.ts +5 -3
- package/extensions/plan/README.md +32 -12
- package/extensions/plan/tools.ts +20 -5
- package/extensions/plan/web-client.ts +58 -17
- package/extensions/plan/web-page.ts +34 -9
- package/extensions/plan/web-router.ts +40 -9
- package/extensions/plan/web.ts +22 -2
- package/extensions/question/README.md +13 -1
- package/extensions/question/answers.ts +25 -18
- package/extensions/question/tools.ts +11 -6
- package/extensions/rewind/README.md +2 -0
- package/extensions/rewind/format.ts +6 -1
- package/extensions/rewind/index.ts +3 -1
- package/extensions/rewind/snapshot-run.ts +1 -1
- package/extensions/subagent/README.md +6 -0
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +56 -29
- package/extensions/todo/README.md +19 -12
- package/extensions/todo/format.ts +17 -25
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/tui.ts +3 -2
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/format.ts +2 -2
- package/extensions/watch/index.ts +28 -23
- package/extensions/watch/tools.ts +5 -3
- package/extensions/watch/tui.ts +12 -7
- package/extensions/web-access/README.md +16 -7
- package/extensions/web-access/fetch/tool.ts +1 -1
- package/extensions/web-access/search/tool.ts +3 -3
- package/extensions/wechat/README.md +6 -0
- package/extensions/worktree/README.md +6 -0
- package/lib/README.md +2 -1
- package/lib/format.ts +17 -0
- package/lib/notice.ts +87 -0
- package/lib/tui/chrome.ts +8 -4
- package/lib/tui/cursor.ts +2 -1
- package/lib/tui/glyphs.ts +10 -11
- package/lib/tui/indent.ts +166 -0
- package/lib/tui/index.ts +3 -1
- package/lib/tui/rows.ts +39 -5
- package/lib/tui/rules.ts +9 -3
- package/lib/tui/tokens.ts +14 -0
- package/lib/web/base.ts +2 -1
- package/lib/web/components.ts +3 -1
- package/lib/web/layout.ts +5 -1
- package/package.json +7 -3
- package/themes/bytefly-dark.json +7 -8
- package/themes/bytefly-light.json +10 -11
- package/assets/bytefly-chrome.png +0 -0
|
@@ -66,6 +66,9 @@ function normalizeRecord(record: JobRecord): JobRecord {
|
|
|
66
66
|
...record,
|
|
67
67
|
statusPath: record.statusPath ?? null,
|
|
68
68
|
startToken: record.startToken ?? null,
|
|
69
|
+
// A registry written before stdin support has no such flag; such a job was
|
|
70
|
+
// started without a pipe, and its session is long gone anyway.
|
|
71
|
+
stdin: record.stdin === true,
|
|
69
72
|
};
|
|
70
73
|
}
|
|
71
74
|
|
|
@@ -18,11 +18,19 @@
|
|
|
18
18
|
* twice and listeners/waiters are removed on cleanup.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
createWriteStream,
|
|
23
|
+
existsSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
renameSync,
|
|
26
|
+
unlinkSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
type WriteStream,
|
|
29
|
+
} from "node:fs";
|
|
22
30
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
23
31
|
import { resolveEffectiveCwd } from "../../lib/env.ts";
|
|
24
32
|
import { loadConfig, type JobsConfig } from "./config.ts";
|
|
25
|
-
import { clipLabel, formatLogs, sanitizeLogLine } from "./format.ts";
|
|
33
|
+
import { clipLabel, compareJobs, formatLogs, sanitizeLogLine } from "./format.ts";
|
|
26
34
|
import { readLogTail } from "./logs.ts";
|
|
27
35
|
import {
|
|
28
36
|
defaultKillTree,
|
|
@@ -34,6 +42,7 @@ import {
|
|
|
34
42
|
type SpawnFn,
|
|
35
43
|
type SpawnedProcess,
|
|
36
44
|
type StartTokenFn,
|
|
45
|
+
type StdinSink,
|
|
37
46
|
} from "./process.ts";
|
|
38
47
|
import {
|
|
39
48
|
loadRegistries,
|
|
@@ -61,6 +70,8 @@ import { createWaiters } from "./waiters.ts";
|
|
|
61
70
|
const LOG_MODEL_CHARS = 16 * 1024;
|
|
62
71
|
/** Cap on the buffered partial line used to track the latest output. */
|
|
63
72
|
const MAX_PENDING = 16 * 1024;
|
|
73
|
+
/** Bytes held while a rotation swaps the log stream before writes resume. */
|
|
74
|
+
const MAX_ROTATION_QUEUE_BYTES = 2 * 1024 * 1024;
|
|
64
75
|
/** Default `wait` timeout. */
|
|
65
76
|
const DEFAULT_WAIT_MS = 30_000;
|
|
66
77
|
|
|
@@ -70,6 +81,8 @@ export interface StartOptions {
|
|
|
70
81
|
label?: string;
|
|
71
82
|
wake?: boolean;
|
|
72
83
|
detached?: boolean;
|
|
84
|
+
/** Give the child a stdin pipe, so `send` can write to it while it runs here. */
|
|
85
|
+
stdin?: boolean;
|
|
73
86
|
/** Auto-kill the job after this many milliseconds (SIGTERM, escalating to SIGKILL). */
|
|
74
87
|
timeoutMs?: number;
|
|
75
88
|
}
|
|
@@ -91,6 +104,23 @@ export interface WaitResult {
|
|
|
91
104
|
cancelled: boolean;
|
|
92
105
|
}
|
|
93
106
|
|
|
107
|
+
export interface SendOptions {
|
|
108
|
+
/** Text written to the job's stdin. */
|
|
109
|
+
input?: string;
|
|
110
|
+
/** Append a newline to `input`; defaults to true. */
|
|
111
|
+
newline?: boolean;
|
|
112
|
+
/** End the job's stdin after writing, so the child sees EOF. */
|
|
113
|
+
eof?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface SendResult {
|
|
117
|
+
job: Job;
|
|
118
|
+
/** Characters of `input` written, excluding any newline this call added. */
|
|
119
|
+
sent: number;
|
|
120
|
+
/** True when this call ended the job's stdin. */
|
|
121
|
+
closed: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
94
124
|
export interface RuntimeOptions {
|
|
95
125
|
spawn?: SpawnFn;
|
|
96
126
|
liveness?: LivenessFn;
|
|
@@ -106,10 +136,26 @@ interface Handle {
|
|
|
106
136
|
pending: string;
|
|
107
137
|
killTimer?: ReturnType<typeof setTimeout>;
|
|
108
138
|
timeoutTimer?: ReturnType<typeof setTimeout>;
|
|
139
|
+
/** The child's stdin pipe, present only for a job started with `stdin: true`. */
|
|
140
|
+
stdin?: StdinSink | null;
|
|
141
|
+
/** True once stdin was ended, or the child closed its end. */
|
|
142
|
+
stdinClosed?: boolean;
|
|
143
|
+
/** Chunks that arrived while the log stream was being swapped. */
|
|
144
|
+
queue: Buffer[];
|
|
145
|
+
/** Bytes currently in `queue`. */
|
|
146
|
+
queuedBytes: number;
|
|
147
|
+
/** True between ending the old stream and reopening the log. */
|
|
148
|
+
rotating: boolean;
|
|
149
|
+
/** Bytes dropped because a rotation could not keep up; reported by `logs`. */
|
|
150
|
+
droppedBytes: number;
|
|
151
|
+
/** Bytes written to the live segment, counted exactly as chunks arrive. */
|
|
152
|
+
logBytes: number;
|
|
109
153
|
}
|
|
110
154
|
|
|
111
155
|
export interface JobsRuntime {
|
|
112
156
|
readonly config: JobsConfig;
|
|
157
|
+
/** This session's id, so a caller can tell its own records from a peer's. */
|
|
158
|
+
readonly sessionId: string;
|
|
113
159
|
/** Effective working directory, honoring an active worktree root. */
|
|
114
160
|
effectiveCwd(ctx: ExtensionContext): string;
|
|
115
161
|
/** Load and reconcile the registry, then paint the UI. */
|
|
@@ -118,6 +164,11 @@ export interface JobsRuntime {
|
|
|
118
164
|
list(): JobRecord[];
|
|
119
165
|
get(id: string): Job | undefined;
|
|
120
166
|
logs(id: string, lines?: number): LogResult | undefined;
|
|
167
|
+
/**
|
|
168
|
+
* Write to a running job's stdin. Throws a model-readable error when the job is
|
|
169
|
+
* gone, was started without `stdin: true`, or belongs to another session.
|
|
170
|
+
*/
|
|
171
|
+
send(id: string, options: SendOptions): SendResult;
|
|
121
172
|
kill(id: string, signal?: KillSignal): Job | undefined;
|
|
122
173
|
wait(
|
|
123
174
|
id: string,
|
|
@@ -177,6 +228,9 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
177
228
|
try {
|
|
178
229
|
const path = store.safeLogPath(job);
|
|
179
230
|
if (path && existsSync(path)) unlinkSync(path);
|
|
231
|
+
// Both segments are the job's own log; drop the rotated one too.
|
|
232
|
+
const rotated = store.safeRotatedLogPath(job);
|
|
233
|
+
if (rotated && existsSync(rotated)) unlinkSync(rotated);
|
|
180
234
|
} catch {
|
|
181
235
|
// Log cleanup is best-effort.
|
|
182
236
|
}
|
|
@@ -369,6 +423,21 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
369
423
|
if (!handle) return;
|
|
370
424
|
if (handle.killTimer) clearTimeout(handle.killTimer);
|
|
371
425
|
if (handle.timeoutTimer) clearTimeout(handle.timeoutTimer);
|
|
426
|
+
// A rotation still in flight checks the handle map before reopening, so
|
|
427
|
+
// dropping the queue here cannot leak a reopened stream for a dead job.
|
|
428
|
+
handle.queue = [];
|
|
429
|
+
handle.queuedBytes = 0;
|
|
430
|
+
// Release the stdin pipe so a child waiting on input sees EOF instead of a
|
|
431
|
+
// half-open pipe nobody can write to.
|
|
432
|
+
if (handle.stdin && !handle.stdinClosed) {
|
|
433
|
+
try {
|
|
434
|
+
handle.stdin.end();
|
|
435
|
+
} catch {
|
|
436
|
+
// Already closed.
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
handle.stdin = null;
|
|
440
|
+
handle.stdinClosed = true;
|
|
372
441
|
if (options.endStream) {
|
|
373
442
|
try {
|
|
374
443
|
handle.stream?.end();
|
|
@@ -383,11 +452,7 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
383
452
|
const handle = handles.get(id);
|
|
384
453
|
const job = jobs.get(id);
|
|
385
454
|
if (!handle || !job) return;
|
|
386
|
-
|
|
387
|
-
handle.stream?.write(chunk);
|
|
388
|
-
} catch {
|
|
389
|
-
// A closed stream must not break output handling.
|
|
390
|
-
}
|
|
455
|
+
writeChunk(handle, job, chunk);
|
|
391
456
|
handle.pending += chunk.toString("utf8");
|
|
392
457
|
if (handle.pending.length > MAX_PENDING) handle.pending = handle.pending.slice(-MAX_PENDING);
|
|
393
458
|
consume(handle, job);
|
|
@@ -398,6 +463,98 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
398
463
|
}
|
|
399
464
|
};
|
|
400
465
|
|
|
466
|
+
const writeLog = (handle: Handle, chunk: Buffer): void => {
|
|
467
|
+
try {
|
|
468
|
+
handle.stream?.write(chunk);
|
|
469
|
+
} catch {
|
|
470
|
+
// A closed stream must not break output handling.
|
|
471
|
+
}
|
|
472
|
+
// Count what we handed the stream: `WriteStream.bytesWritten` only updates on
|
|
473
|
+
// its completion callback, which can lag a tick behind the file on disk.
|
|
474
|
+
if (handle.stream) handle.logBytes += chunk.length;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/** Buffer a chunk while the log stream is being swapped, dropping the oldest past the cap. */
|
|
478
|
+
const enqueue = (handle: Handle, chunk: Buffer, front = false): void => {
|
|
479
|
+
if (front) handle.queue.unshift(chunk);
|
|
480
|
+
else handle.queue.push(chunk);
|
|
481
|
+
handle.queuedBytes += chunk.length;
|
|
482
|
+
while (handle.queuedBytes > MAX_ROTATION_QUEUE_BYTES && handle.queue.length > 1) {
|
|
483
|
+
const dropped = handle.queue.shift();
|
|
484
|
+
if (!dropped) break;
|
|
485
|
+
handle.queuedBytes -= dropped.length;
|
|
486
|
+
handle.droppedBytes += dropped.length;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Move the live segment aside, queueing `chunk` ahead of the backlog, and reopen.
|
|
492
|
+
*
|
|
493
|
+
* Returns false when no rotation can start (no stream, or a record whose log
|
|
494
|
+
* paths do not validate), so the caller writes the chunk instead of stranding it
|
|
495
|
+
* in a queue nothing will drain.
|
|
496
|
+
*
|
|
497
|
+
* The old stream is ended and its `close` awaited before the rename, because
|
|
498
|
+
* Windows refuses to rename an open file; chunks that arrive in that window
|
|
499
|
+
* are queued rather than written, so nothing lands in the renamed inode.
|
|
500
|
+
*/
|
|
501
|
+
const rotateLog = (handle: Handle, job: Job, chunk: Buffer): boolean => {
|
|
502
|
+
const stream = handle.stream;
|
|
503
|
+
const logPath = store.safeLogPath(job);
|
|
504
|
+
const rotated = store.safeRotatedLogPath(job);
|
|
505
|
+
if (!stream || !logPath || !rotated || handle.rotating) return false;
|
|
506
|
+
// This chunk precedes whatever queued while the previous segment was closing.
|
|
507
|
+
enqueue(handle, chunk, true);
|
|
508
|
+
handle.rotating = true;
|
|
509
|
+
stream.once("close", () => {
|
|
510
|
+
// A killed or cleared job drops its handle; never reopen for it.
|
|
511
|
+
if (handles.get(job.id) !== handle) return;
|
|
512
|
+
try {
|
|
513
|
+
if (existsSync(rotated)) unlinkSync(rotated);
|
|
514
|
+
renameSync(logPath, rotated);
|
|
515
|
+
} catch {
|
|
516
|
+
// A failed rename leaves the oversized segment in place; keep appending.
|
|
517
|
+
}
|
|
518
|
+
const reopened = createWriteStream(logPath, { flags: "a" });
|
|
519
|
+
reopened.on("error", () => {});
|
|
520
|
+
handle.stream = reopened;
|
|
521
|
+
handle.rotating = false;
|
|
522
|
+
handle.logBytes = 0;
|
|
523
|
+
drain(handle, job);
|
|
524
|
+
});
|
|
525
|
+
try {
|
|
526
|
+
stream.end();
|
|
527
|
+
} catch {
|
|
528
|
+
// Already ended.
|
|
529
|
+
}
|
|
530
|
+
return true;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
/** Write one output chunk, rotating the segment first when it reached the cap. */
|
|
534
|
+
const writeChunk = (handle: Handle, job: Job, chunk: Buffer): void => {
|
|
535
|
+
if (handle.rotating) {
|
|
536
|
+
// Later than everything already queued.
|
|
537
|
+
enqueue(handle, chunk);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const cap = config.maxLogBytes;
|
|
541
|
+
if (handle.stream && cap > 0 && handle.logBytes >= cap && rotateLog(handle, job, chunk)) return;
|
|
542
|
+
writeLog(handle, chunk);
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Replay the queue through `writeChunk` after a rotation, so a backlog that is
|
|
547
|
+
* itself larger than the cap rotates again instead of filling one segment.
|
|
548
|
+
*/
|
|
549
|
+
const drain = (handle: Handle, job: Job): void => {
|
|
550
|
+
while (!handle.rotating && handle.queue.length > 0) {
|
|
551
|
+
const chunk = handle.queue.shift();
|
|
552
|
+
if (!chunk) break;
|
|
553
|
+
handle.queuedBytes -= chunk.length;
|
|
554
|
+
writeChunk(handle, job, chunk);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
401
558
|
const finalize = (id: string, code: number | null, signal: string | null): void => {
|
|
402
559
|
const handle = handles.get(id);
|
|
403
560
|
const job = jobs.get(id);
|
|
@@ -447,12 +604,16 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
447
604
|
get config() {
|
|
448
605
|
return config;
|
|
449
606
|
},
|
|
607
|
+
get sessionId() {
|
|
608
|
+
return sessionId;
|
|
609
|
+
},
|
|
450
610
|
effectiveCwd,
|
|
451
611
|
load,
|
|
452
612
|
start,
|
|
453
|
-
list: () =>
|
|
613
|
+
list: () => [...jobs.values()].sort(compareJobs).map(toRecord),
|
|
454
614
|
get: (id) => jobs.get(id),
|
|
455
615
|
logs,
|
|
616
|
+
send,
|
|
456
617
|
kill,
|
|
457
618
|
wait,
|
|
458
619
|
clear,
|
|
@@ -555,6 +716,14 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
555
716
|
function start(startOptions: StartOptions, ctx: ExtensionContext): Job {
|
|
556
717
|
if (!config.enabled) throw new Error("jobs are disabled.");
|
|
557
718
|
ui.attach(ctx);
|
|
719
|
+
const wantsStdin = startOptions.stdin === true;
|
|
720
|
+
const detached = startOptions.detached ?? config.detachedByDefault;
|
|
721
|
+
// A detached job outlives this process, so a stdin pipe owned by this session
|
|
722
|
+
// would hand it EOF the moment the session ends; refuse instead of silently
|
|
723
|
+
// dropping the pipe (or the detach).
|
|
724
|
+
if (wantsStdin && detached) {
|
|
725
|
+
throw new Error("a detached job cannot take stdin; start it with `detached: false`.");
|
|
726
|
+
}
|
|
558
727
|
const cwd = startOptions.cwd ? startOptions.cwd : effectiveCwd(ctx);
|
|
559
728
|
// The registry directory must exist before `nextId`, which reserves the id
|
|
560
729
|
// with an `O_EXCL` lock file inside it.
|
|
@@ -564,7 +733,6 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
564
733
|
const id = store.nextId(jobs.keys());
|
|
565
734
|
const logPath = store.logPath(id);
|
|
566
735
|
const statusPath = process.platform === "win32" ? null : store.statusPath(id);
|
|
567
|
-
const detached = startOptions.detached ?? config.detachedByDefault;
|
|
568
736
|
// A detached POSIX job points its own output at the log, so it keeps logging
|
|
569
737
|
// and survives after this process ends; Windows keeps the parent pipe.
|
|
570
738
|
const detachOutput = detached && process.platform !== "win32";
|
|
@@ -577,7 +745,7 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
577
745
|
if (detachOutput) env[LOG_ENV] = logPath;
|
|
578
746
|
let process_: SpawnedProcess;
|
|
579
747
|
try {
|
|
580
|
-
process_ = spawn(program, { cwd, env, detachOutput });
|
|
748
|
+
process_ = spawn(program, { cwd, env, detachOutput, stdin: wantsStdin });
|
|
581
749
|
} catch (error) {
|
|
582
750
|
// A failed spawn must not leave the reserved id behind.
|
|
583
751
|
store.releaseId(id);
|
|
@@ -612,6 +780,7 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
612
780
|
statusPath,
|
|
613
781
|
detached,
|
|
614
782
|
wake: startOptions.wake ?? config.wakeOnFinish,
|
|
783
|
+
stdin: wantsStdin,
|
|
615
784
|
sessionId,
|
|
616
785
|
seen: false,
|
|
617
786
|
lastLine: "",
|
|
@@ -619,7 +788,28 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
619
788
|
owned: true,
|
|
620
789
|
};
|
|
621
790
|
|
|
622
|
-
const handle: Handle = {
|
|
791
|
+
const handle: Handle = {
|
|
792
|
+
stream,
|
|
793
|
+
pending: "",
|
|
794
|
+
queue: [],
|
|
795
|
+
queuedBytes: 0,
|
|
796
|
+
rotating: false,
|
|
797
|
+
droppedBytes: 0,
|
|
798
|
+
logBytes: 0,
|
|
799
|
+
};
|
|
800
|
+
if (wantsStdin) {
|
|
801
|
+
const sink = process_.stdin ?? null;
|
|
802
|
+
handle.stdin = sink;
|
|
803
|
+
handle.stdinClosed = sink === null;
|
|
804
|
+
// A child that exits can emit `error`/`close` on the pipe; record it so a
|
|
805
|
+
// later `send` reports the truth instead of throwing an unhandled stream error.
|
|
806
|
+
const markClosed = (): void => {
|
|
807
|
+
const current = handles.get(id);
|
|
808
|
+
if (current) current.stdinClosed = true;
|
|
809
|
+
};
|
|
810
|
+
sink?.on("error", markClosed);
|
|
811
|
+
sink?.on("close", markClosed);
|
|
812
|
+
}
|
|
623
813
|
handles.set(id, handle);
|
|
624
814
|
jobs.set(id, job);
|
|
625
815
|
|
|
@@ -643,16 +833,33 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
643
833
|
function logs(id: string, lines = config.maxLogLines): LogResult | undefined {
|
|
644
834
|
const job = jobs.get(id);
|
|
645
835
|
if (!job) return undefined;
|
|
646
|
-
const
|
|
647
|
-
|
|
836
|
+
const current = readLogTail(store.safeLogPath(job), lines);
|
|
837
|
+
let all = current.lines;
|
|
838
|
+
let more = current.more;
|
|
839
|
+
let truncated = current.truncated;
|
|
840
|
+
// Keep `logs` continuous across a rotation: fill the request from the
|
|
841
|
+
// previous segment when the live one holds fewer lines.
|
|
842
|
+
if (all.length < lines) {
|
|
843
|
+
const previous = readLogTail(store.safeRotatedLogPath(job), lines - all.length);
|
|
844
|
+
if (previous.lines.length > 0) {
|
|
845
|
+
all = [...previous.lines, ...all];
|
|
846
|
+
more = more || previous.more;
|
|
847
|
+
truncated = truncated || previous.truncated;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const last = all.at(-1);
|
|
648
851
|
if (last) job.lastLine = last;
|
|
649
|
-
const formatted = formatLogs(job,
|
|
852
|
+
const formatted = formatLogs(job, all, LOG_MODEL_CHARS);
|
|
853
|
+
// A rotation that outran its drain drops the oldest queued chunks; say so once
|
|
854
|
+
// rather than letting the gap look like a program that skipped output.
|
|
855
|
+
const dropped = handles.get(id)?.droppedBytes ?? 0;
|
|
856
|
+
const text = dropped > 0 ? `${formatted.text}\n… ${dropped} bytes dropped while the log rotated.` : formatted.text;
|
|
650
857
|
return {
|
|
651
858
|
job,
|
|
652
|
-
text
|
|
653
|
-
lines:
|
|
654
|
-
truncated: formatted.truncated ||
|
|
655
|
-
more
|
|
859
|
+
text,
|
|
860
|
+
lines: all,
|
|
861
|
+
truncated: formatted.truncated || truncated,
|
|
862
|
+
more,
|
|
656
863
|
};
|
|
657
864
|
}
|
|
658
865
|
|
|
@@ -725,6 +932,34 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
725
932
|
return job;
|
|
726
933
|
}
|
|
727
934
|
|
|
935
|
+
function send(id: string, options: SendOptions): SendResult {
|
|
936
|
+
const job = jobs.get(id);
|
|
937
|
+
if (!job) throw new Error(`no job "${id}".`);
|
|
938
|
+
if (job.status !== "running") throw new Error(`job ${id} is ${job.status}; stdin is closed.`);
|
|
939
|
+
if (!job.stdin) throw new Error(`job ${id} was not started with stdin; restart it with \`stdin: true\`.`);
|
|
940
|
+
const handle = handles.get(id);
|
|
941
|
+
// A pipe this session opened but that is closed is a different fact from one
|
|
942
|
+
// that was never ours, so state it first.
|
|
943
|
+
if (handle?.stdinClosed) throw new Error(`job ${id} closed its stdin.`);
|
|
944
|
+
if (!handle?.stdin) throw new Error(`job ${id}'s stdin belongs to the session that started it.`);
|
|
945
|
+
|
|
946
|
+
const input = options.input ?? "";
|
|
947
|
+
const payload = input && options.newline !== false ? `${input}\n` : input;
|
|
948
|
+
try {
|
|
949
|
+
if (payload) handle.stdin.write(payload);
|
|
950
|
+
if (options.eof) {
|
|
951
|
+
handle.stdin.end();
|
|
952
|
+
handle.stdinClosed = true;
|
|
953
|
+
}
|
|
954
|
+
} catch {
|
|
955
|
+
// The child closed its end; record it so the next `send` says so.
|
|
956
|
+
handle.stdinClosed = true;
|
|
957
|
+
handle.stdin = null;
|
|
958
|
+
throw new Error(`job ${id} closed its stdin.`);
|
|
959
|
+
}
|
|
960
|
+
return { job, sent: input.length, closed: options.eof === true };
|
|
961
|
+
}
|
|
962
|
+
|
|
728
963
|
function wait(
|
|
729
964
|
id: string,
|
|
730
965
|
timeoutMs: number | undefined,
|
package/extensions/job/schema.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { JOB_ACTIONS, KILL_SIGNALS, type JobAction, type KillSignal } from "./ty
|
|
|
14
14
|
export const MAX_COMMAND = 4000;
|
|
15
15
|
/** Longest accepted label. */
|
|
16
16
|
export const MAX_LABEL = 120;
|
|
17
|
+
/** Longest accepted `send.input`, in characters. */
|
|
18
|
+
export const MAX_STDIN = 8192;
|
|
17
19
|
/** Bounds for `logs.lines`. */
|
|
18
20
|
const MIN_LOG_LINES = 1;
|
|
19
21
|
const MAX_LOG_LINES = 2000;
|
|
@@ -31,10 +33,20 @@ export const JobParams = Type.Object({
|
|
|
31
33
|
label: Type.Optional(Type.String({ description: "Short display label; defaults to the command." })),
|
|
32
34
|
wake: Type.Optional(Type.Boolean({ description: "Wake the agent with one triggered turn when the job finishes." })),
|
|
33
35
|
detached: Type.Optional(Type.Boolean({ description: "Leave the job running when the session ends." })),
|
|
36
|
+
stdin: Type.Optional(
|
|
37
|
+
Type.Boolean({
|
|
38
|
+
description: "Give the job a stdin pipe so `send` can write to it (for `start`); not with `detached`.",
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
34
41
|
id: Type.Optional(Type.String({ description: "Job id, e.g. `j1`." })),
|
|
35
42
|
lines: Type.Optional(
|
|
36
43
|
Type.Integer({ minimum: MIN_LOG_LINES, maximum: MAX_LOG_LINES, description: "Log lines to return." }),
|
|
37
44
|
),
|
|
45
|
+
input: Type.Optional(
|
|
46
|
+
Type.String({ description: `Text to write to a running job's stdin (for \`send\`; max ${MAX_STDIN} characters).` }),
|
|
47
|
+
),
|
|
48
|
+
newline: Type.Optional(Type.Boolean({ description: "Append a newline to `input` (for `send`; default true)." })),
|
|
49
|
+
eof: Type.Optional(Type.Boolean({ description: "End the job's stdin after writing, so it sees EOF (for `send`)." })),
|
|
38
50
|
signal: Type.Optional(StringEnum(KILL_SIGNALS, { description: "Signal for `kill`; defaults to SIGTERM." })),
|
|
39
51
|
timeoutMs: Type.Optional(
|
|
40
52
|
Type.Integer({
|
|
@@ -64,11 +76,13 @@ export type JobCall =
|
|
|
64
76
|
label?: string;
|
|
65
77
|
wake?: boolean;
|
|
66
78
|
detached?: boolean;
|
|
79
|
+
stdin?: boolean;
|
|
67
80
|
timeoutMs?: number;
|
|
68
81
|
}
|
|
69
82
|
| { action: "list" }
|
|
70
83
|
| { action: "status"; id: string }
|
|
71
84
|
| { action: "logs"; id: string; lines?: number }
|
|
85
|
+
| { action: "send"; id: string; input?: string; newline?: boolean; eof?: boolean }
|
|
72
86
|
| { action: "kill"; id: string; signal?: KillSignal }
|
|
73
87
|
| { action: "wait"; id: string; timeoutMs?: number }
|
|
74
88
|
| { action: "clear"; id?: string; all?: boolean };
|
|
@@ -110,6 +124,7 @@ export function normalizeCall(raw: unknown): JobCall {
|
|
|
110
124
|
...(cwd ? { cwd } : {}),
|
|
111
125
|
...(typeof args.wake === "boolean" ? { wake: args.wake } : {}),
|
|
112
126
|
...(typeof args.detached === "boolean" ? { detached: args.detached } : {}),
|
|
127
|
+
...(typeof args.stdin === "boolean" ? { stdin: args.stdin } : {}),
|
|
113
128
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
114
129
|
};
|
|
115
130
|
}
|
|
@@ -124,6 +139,25 @@ export function normalizeCall(raw: unknown): JobCall {
|
|
|
124
139
|
}
|
|
125
140
|
return { action: "logs", id: requireId(), ...(lines !== undefined ? { lines } : {}) };
|
|
126
141
|
}
|
|
142
|
+
case "send": {
|
|
143
|
+
const jobId = requireId();
|
|
144
|
+
if (args.input !== undefined && typeof args.input !== "string") {
|
|
145
|
+
throw new Error('"send" input must be a string.');
|
|
146
|
+
}
|
|
147
|
+
const input = typeof args.input === "string" ? args.input : undefined;
|
|
148
|
+
if (input !== undefined && input.length > MAX_STDIN) {
|
|
149
|
+
throw new Error(`input is longer than ${MAX_STDIN} characters.`);
|
|
150
|
+
}
|
|
151
|
+
const eof = args.eof === true;
|
|
152
|
+
if (!input && !eof) throw new Error('"send" requires `input` or `eof: true`.');
|
|
153
|
+
return {
|
|
154
|
+
action: "send",
|
|
155
|
+
id: jobId,
|
|
156
|
+
...(input ? { input } : {}),
|
|
157
|
+
...(typeof args.newline === "boolean" ? { newline: args.newline } : {}),
|
|
158
|
+
...(eof ? { eof } : {}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
127
161
|
case "kill": {
|
|
128
162
|
const signal = args.signal;
|
|
129
163
|
if (signal !== undefined && !(KILL_SIGNALS as readonly string[]).includes(signal)) {
|
package/extensions/job/store.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* is otherwise running fine.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { join, resolve } from "node:path";
|
|
14
|
+
import { join, resolve, sep } from "node:path";
|
|
15
15
|
import { REGISTRY_VERSION, loadRegistries, registryDirFor, saveRegistry } from "./registry.ts";
|
|
16
16
|
import { listReservedIds, pruneReservations, releaseReservation, reserveId } from "./reservations.ts";
|
|
17
17
|
import { statusPathFor } from "./paths.ts";
|
|
@@ -38,6 +38,10 @@ export interface JobStore {
|
|
|
38
38
|
logPath(id: string): string;
|
|
39
39
|
/** The log path only when it is exactly this job's file in the directory. */
|
|
40
40
|
safeLogPath(job: JobRecord): string | undefined;
|
|
41
|
+
/** Absolute path of the segment a rotation moved the previous log to. */
|
|
42
|
+
rotatedLogPath(id: string): string;
|
|
43
|
+
/** The rotated path only when it stays inside the directory. */
|
|
44
|
+
safeRotatedLogPath(job: JobRecord): string | undefined;
|
|
41
45
|
/** Absolute status path for a job id in the current directory. */
|
|
42
46
|
statusPath(id: string): string;
|
|
43
47
|
/** The status path only when it is exactly this job's file in the directory. */
|
|
@@ -84,6 +88,14 @@ export function createJobStore(initialDir = registryDirFor(process.cwd())): JobS
|
|
|
84
88
|
},
|
|
85
89
|
logPath: (id) => join(dir, `${id}.log`),
|
|
86
90
|
safeLogPath: (job) => safeArtifact(job.logPath, join(dir, `${job.id}.log`)),
|
|
91
|
+
rotatedLogPath: (id) => join(dir, `${id}.log.1`),
|
|
92
|
+
// No record field names this file, so the id is the only input: refuse an id
|
|
93
|
+
// that would resolve outside the registry directory.
|
|
94
|
+
safeRotatedLogPath: (job) => {
|
|
95
|
+
const root = resolve(dir);
|
|
96
|
+
const target = resolve(root, `${job.id}.log.1`);
|
|
97
|
+
return target.startsWith(`${root}${sep}`) ? target : undefined;
|
|
98
|
+
},
|
|
87
99
|
statusPath: (id) => statusPathFor(dir, id),
|
|
88
100
|
safeStatusPath: (job) => safeArtifact(job.statusPath ?? undefined, join(dir, `${job.id}.status`)),
|
|
89
101
|
};
|
package/extensions/job/tools.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model-facing registration for the `job` tool.
|
|
3
3
|
*
|
|
4
|
-
* One tool with
|
|
5
|
-
* `wait`, `clear`. A background process can outlive the tool call, so
|
|
6
|
-
* returns immediately with an id; the model polls with `status`/`logs` or
|
|
7
|
-
* blocks with `wait`.
|
|
4
|
+
* One tool with eight actions: `start`, `list`, `status`, `logs`, `send`,
|
|
5
|
+
* `kill`, `wait`, `clear`. A background process can outlive the tool call, so
|
|
6
|
+
* `start` returns immediately with an id; the model polls with `status`/`logs` or
|
|
7
|
+
* blocks with `wait`. `stdin: true` opens a pipe that `send` writes to. Failures
|
|
8
|
+
* come back as an error result.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
12
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
12
13
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
14
|
+
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
13
15
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
14
16
|
import {
|
|
15
17
|
compareJobs,
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
formatJobList,
|
|
19
21
|
formatJobOutcomeLine,
|
|
20
22
|
formatJobStatus,
|
|
23
|
+
shortLabel,
|
|
21
24
|
} from "./format.ts";
|
|
22
25
|
import type { JobsRuntime } from "./runtime.ts";
|
|
23
26
|
import { JobParams, normalizeCall, type JobArgs, type JobCall } from "./schema.ts";
|
|
@@ -48,6 +51,7 @@ function errorResult(
|
|
|
48
51
|
const GUIDELINES = [
|
|
49
52
|
"Use `job start` for long-running or background commands (builds, test suites, servers, watchers) so the turn is not blocked.",
|
|
50
53
|
"Poll with `job status`/`job logs`, or use `job wait` when you must have the result before continuing.",
|
|
54
|
+
"Start a job with `stdin: true` to drive an interactive program: `job send` writes a line to it and `eof: true` closes its input.",
|
|
51
55
|
"Use `job kill` to stop a running job and `job clear` to drop finished ones.",
|
|
52
56
|
];
|
|
53
57
|
|
|
@@ -56,9 +60,9 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
56
60
|
name: TOOL_NAME,
|
|
57
61
|
label: "Job",
|
|
58
62
|
description:
|
|
59
|
-
"Manage background shell-command jobs. Actions: `start` (returns a job id immediately
|
|
60
|
-
"`kill`/`wait` (by id), and `clear`. Jobs are killed when the session ends unless started `detached`.",
|
|
61
|
-
promptSnippet: "Run and manage background shell-command jobs (start, list, status, logs, kill, wait, clear).",
|
|
63
|
+
"Manage background shell-command jobs. Actions: `start` (returns a job id immediately; `stdin: true` opens a stdin pipe), " +
|
|
64
|
+
"`list`, `status`/`logs`/`send`/`kill`/`wait` (by id), and `clear`. Jobs are killed when the session ends unless started `detached`.",
|
|
65
|
+
promptSnippet: "Run and manage background shell-command jobs (start, list, status, logs, send, kill, wait, clear).",
|
|
62
66
|
promptGuidelines: GUIDELINES,
|
|
63
67
|
parameters: JobParams,
|
|
64
68
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
@@ -82,6 +86,7 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
82
86
|
label: call.label,
|
|
83
87
|
wake: call.wake,
|
|
84
88
|
detached: call.detached,
|
|
89
|
+
stdin: call.stdin,
|
|
85
90
|
timeoutMs: call.timeoutMs,
|
|
86
91
|
},
|
|
87
92
|
ctx,
|
|
@@ -100,8 +105,8 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
100
105
|
case "list": {
|
|
101
106
|
const jobs = runtime.list();
|
|
102
107
|
return {
|
|
103
|
-
content: [{ type: "text" as const, text: formatJobList(jobs) }],
|
|
104
|
-
details: { action: "list", jobs } satisfies JobDetails,
|
|
108
|
+
content: [{ type: "text" as const, text: formatJobList(jobs, Date.now(), runtime.sessionId) }],
|
|
109
|
+
details: { action: "list", jobs, sessionId: runtime.sessionId } satisfies JobDetails,
|
|
105
110
|
};
|
|
106
111
|
}
|
|
107
112
|
|
|
@@ -109,8 +114,8 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
109
114
|
const job = runtime.get(call.id);
|
|
110
115
|
if (!job) return errorResult("status", `no job "${call.id}".`);
|
|
111
116
|
return {
|
|
112
|
-
content: [{ type: "text" as const, text: formatJobStatus(job) }],
|
|
113
|
-
details: { action: "status", job: toRecord(job) } satisfies JobDetails,
|
|
117
|
+
content: [{ type: "text" as const, text: formatJobStatus(job, Date.now(), runtime.sessionId) }],
|
|
118
|
+
details: { action: "status", job: toRecord(job), sessionId: runtime.sessionId } satisfies JobDetails,
|
|
114
119
|
};
|
|
115
120
|
}
|
|
116
121
|
|
|
@@ -128,6 +133,30 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
128
133
|
};
|
|
129
134
|
}
|
|
130
135
|
|
|
136
|
+
case "send": {
|
|
137
|
+
const result = runtime.send(call.id, {
|
|
138
|
+
input: call.input,
|
|
139
|
+
newline: call.newline,
|
|
140
|
+
eof: call.eof,
|
|
141
|
+
});
|
|
142
|
+
const job = result.job;
|
|
143
|
+
const text =
|
|
144
|
+
result.sent > 0
|
|
145
|
+
? `Sent ${result.sent} character${result.sent === 1 ? "" : "s"} to ${job.id} (${shortLabel(job)})${
|
|
146
|
+
result.closed ? " and closed its stdin" : ""
|
|
147
|
+
}.`
|
|
148
|
+
: `Closed ${job.id}'s stdin.`;
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text" as const, text }],
|
|
151
|
+
details: {
|
|
152
|
+
action: "send",
|
|
153
|
+
job: toRecord(job),
|
|
154
|
+
sent: result.sent,
|
|
155
|
+
stdinClosed: result.closed,
|
|
156
|
+
} satisfies JobDetails,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
131
160
|
case "kill": {
|
|
132
161
|
const before = runtime.get(call.id);
|
|
133
162
|
if (!before) return errorResult("kill", `no job "${call.id}".`);
|
|
@@ -209,8 +238,9 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
209
238
|
const text = reuseText(context.lastComponent);
|
|
210
239
|
text.setText(
|
|
211
240
|
theme.fg("toolTitle", theme.bold(`${TOOL_NAME} `)) +
|
|
212
|
-
|
|
213
|
-
|
|
241
|
+
paint(
|
|
242
|
+
theme,
|
|
243
|
+
"meta",
|
|
214
244
|
sanitizeLine(formatCallText(action, args as Record<string, unknown>, context.argsComplete)),
|
|
215
245
|
),
|
|
216
246
|
);
|
|
@@ -256,7 +286,12 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
256
286
|
}
|
|
257
287
|
const ordered = [...details.jobs].sort(compareJobs);
|
|
258
288
|
const shown = options.expanded ? ordered : ordered.slice(0, RESULT_ROWS);
|
|
259
|
-
const input: JobResultInput = {
|
|
289
|
+
const input: JobResultInput = {
|
|
290
|
+
kind: "list",
|
|
291
|
+
jobs: shown,
|
|
292
|
+
more: ordered.length - shown.length,
|
|
293
|
+
sessionId: details.sessionId,
|
|
294
|
+
};
|
|
260
295
|
const view = context.lastComponent instanceof JobResult ? context.lastComponent : new JobResult(input, theme);
|
|
261
296
|
view.setInput(input, theme);
|
|
262
297
|
return view;
|
|
@@ -278,11 +313,16 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
278
313
|
line += paint(theme, "warn", " · wait cancelled; job still running");
|
|
279
314
|
} else if (details.action === "kill" && details.signalled) {
|
|
280
315
|
line += paint(theme, "warn", ` · ${details.signal ?? "SIGTERM"} sent`);
|
|
316
|
+
} else if (details.action === "send") {
|
|
317
|
+
const badges: string[] = [];
|
|
318
|
+
if (details.sent) badges.push(`sent ${details.sent}`);
|
|
319
|
+
if (details.stdinClosed) badges.push("stdin closed");
|
|
320
|
+
if (badges.length > 0) line += paint(theme, "faint", `${SEPARATORS.item}${badges.join(SEPARATORS.item)}`);
|
|
281
321
|
} else if (details.action === "start") {
|
|
282
322
|
const badges: string[] = [];
|
|
283
323
|
if (details.job.detached) badges.push("detached");
|
|
284
324
|
if (details.job.wake) badges.push("wake");
|
|
285
|
-
if (badges.length > 0) line += paint(theme, "faint",
|
|
325
|
+
if (badges.length > 0) line += paint(theme, "faint", `${SEPARATORS.item}${badges.join(SEPARATORS.item)}`);
|
|
286
326
|
}
|
|
287
327
|
text.setText(line);
|
|
288
328
|
return text;
|