@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 +46 -30
- package/README.zh-CN.md +40 -25
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -23
- package/docs/extensions/board.md +397 -168
- package/docs/extensions/bytefly.md +234 -197
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +57 -63
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +197 -140
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +145 -0
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +83 -44
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +79 -25
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +17 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +5 -20
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +65 -93
- package/extensions/board/result.ts +30 -39
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +49 -11
- package/extensions/board/snapshot.ts +8 -6
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +154 -55
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +36 -42
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +35 -16
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +90 -28
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +38 -13
- package/extensions/board/view/render.ts +219 -277
- package/extensions/board/view/styles.ts +130 -32
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +7 -8
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +10 -7
- package/extensions/bytefly/footer/render/segments.ts +41 -38
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/render.ts +10 -1
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +89 -79
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -4
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/html.ts +73 -40
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +13 -14
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +9 -9
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +21 -9
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +58 -51
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +61 -41
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +102 -2
- package/extensions/plan/web-collab.ts +4 -4
- package/extensions/plan/web-comments.ts +39 -8
- package/extensions/plan/web-composer.ts +14 -3
- package/extensions/plan/web-keys.ts +149 -25
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +10 -68
- package/extensions/plan/web-outline.ts +22 -2
- package/extensions/plan/web-page.ts +56 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +23 -13
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web-settle.ts +14 -7
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +67 -131
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +39 -0
- package/extensions/serve/index.ts +85 -0
- package/extensions/serve/registry.ts +260 -0
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +17 -9
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -5
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +28 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +30 -5
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +199 -51
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +119 -36
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +81 -63
- package/lib/bus-request.ts +83 -0
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +17 -11
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +30 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +46 -21
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +10 -10
- package/lib/session-label.ts +129 -7
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +39 -22
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +29 -11
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +11 -11
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +33 -4
- package/lib/web/README.md +21 -0
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +20 -2
- package/lib/web/runtime.ts +9 -10
- package/package.json +8 -2
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/config.ts +0 -36
- package/extensions/board/seat.ts +0 -28
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The job runtime's periodic work: the repaint clock, and the sweep that polls
|
|
3
|
+
* the jobs this session reattached.
|
|
4
|
+
*
|
|
5
|
+
* The clock is the only periodic hook the runtime arms. While a job runs it
|
|
6
|
+
* re-asserts the footer chip, touches this session's liveness marker, and lets
|
|
7
|
+
* the runtime poll the jobs it reattached; without it, anything that clears
|
|
8
|
+
* extension statuses (a session reload, another UI path) would hide a
|
|
9
|
+
* still-running job's chip until the next transition.
|
|
10
|
+
*
|
|
11
|
+
* Every fact it acts on is the caller's and arrives as a function: whether a job
|
|
12
|
+
* is running, whether the runtime was disposed, what a tick does, and what a
|
|
13
|
+
* repaint does. Nothing here holds state across calls — the armed timer lives in
|
|
14
|
+
* the returned closure, never at module scope, because Pi loads each extension
|
|
15
|
+
* with its own module cache.
|
|
16
|
+
*
|
|
17
|
+
* The sweep is here because it is the tick's work: a reattached job has no
|
|
18
|
+
* `close` event, so only a periodic pass can notice that its pid is gone.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { defaultInterval, type RepeatTimer } from "../../lib/timer.ts";
|
|
22
|
+
import type { Job, JobRecord, JobStatus } from "./types.ts";
|
|
23
|
+
|
|
24
|
+
export interface RepaintClockDeps {
|
|
25
|
+
/** The repaint interval, read when the clock is armed. */
|
|
26
|
+
repaintMs: () => number;
|
|
27
|
+
/** Whether any record is running, which is the only thing that keeps the clock armed. */
|
|
28
|
+
running: () => boolean;
|
|
29
|
+
/** Whether the runtime was shut down; a disposed runtime arms nothing. */
|
|
30
|
+
disposed: () => boolean;
|
|
31
|
+
/** Refresh this session's liveness marker. */
|
|
32
|
+
touchMarker: () => void;
|
|
33
|
+
/** One tick: the external poll and the chip re-assert. */
|
|
34
|
+
tick: () => void;
|
|
35
|
+
/** One repaint of the surface. */
|
|
36
|
+
repaint: () => void;
|
|
37
|
+
/** The repeating timer, injectable so a test ticks by hand instead of waiting. */
|
|
38
|
+
interval?: RepeatTimer;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RepaintClock {
|
|
42
|
+
/** Arm the clock when a job is running and none is armed, and stop it when nothing is. */
|
|
43
|
+
ensure(): void;
|
|
44
|
+
/** Cancel a pending tick. */
|
|
45
|
+
stop(): void;
|
|
46
|
+
/** Ensure the clock, touch the marker, then repaint — unless the runtime is disposed. */
|
|
47
|
+
paint(): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createRepaintClock(deps: RepaintClockDeps): RepaintClock {
|
|
51
|
+
const interval = deps.interval ?? defaultInterval;
|
|
52
|
+
let cancel: (() => void) | undefined;
|
|
53
|
+
|
|
54
|
+
const stop = (): void => {
|
|
55
|
+
cancel?.();
|
|
56
|
+
cancel = undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const ensure = (): void => {
|
|
60
|
+
if (deps.disposed() || !deps.running()) {
|
|
61
|
+
stop();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (cancel) return;
|
|
65
|
+
cancel = interval(deps.repaintMs(), () => deps.tick());
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const paint = (): void => {
|
|
69
|
+
ensure();
|
|
70
|
+
deps.touchMarker();
|
|
71
|
+
if (deps.disposed()) return;
|
|
72
|
+
deps.repaint();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return { ensure, stop, paint };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** What the sweep asks of the table; every decision it makes is answered there. */
|
|
79
|
+
export interface ExternalSweepDeps {
|
|
80
|
+
/** Every record in the table. */
|
|
81
|
+
records: () => Iterable<Job>;
|
|
82
|
+
/** This session's id, so a peer's record is told from one's own. */
|
|
83
|
+
sessionId: () => string;
|
|
84
|
+
/**
|
|
85
|
+
* Whether the session that owns a record is still here: `live` and `quiet` both count,
|
|
86
|
+
* because a heartbeat only ticks while a job runs.
|
|
87
|
+
*/
|
|
88
|
+
ownerPresent: (sessionId: string) => boolean;
|
|
89
|
+
/** Whether a pid is still running. */
|
|
90
|
+
alive: (pid: number) => boolean;
|
|
91
|
+
/** Whether a persisted start-time token still matches the pid it names. */
|
|
92
|
+
owns: (job: JobRecord) => boolean;
|
|
93
|
+
/** Settle a record whose process is gone; false when it cannot be judged. */
|
|
94
|
+
settleGone: (job: Job) => boolean;
|
|
95
|
+
/** Move a record to a settled status, resolving its waiters. */
|
|
96
|
+
settle: (job: Job, patch: { status: JobStatus; signal?: string | null }) => void;
|
|
97
|
+
/** SIGTERM a pid. */
|
|
98
|
+
signal: (pid: number) => void;
|
|
99
|
+
/** Refresh this session's liveness marker. */
|
|
100
|
+
touchMarker: () => void;
|
|
101
|
+
/** Prune finished records, persist, and repaint, after something changed. */
|
|
102
|
+
changed: () => void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Poll the reattached (unowned) jobs once: settle any whose pid is gone, and reap
|
|
107
|
+
* an abandoned non-detached process whose owner session is gone.
|
|
108
|
+
*/
|
|
109
|
+
export function createExternalSweep(deps: ExternalSweepDeps): () => void {
|
|
110
|
+
return () => {
|
|
111
|
+
deps.touchMarker();
|
|
112
|
+
let changed = false;
|
|
113
|
+
for (const job of deps.records()) {
|
|
114
|
+
if (job.status !== "running" || job.owned) continue;
|
|
115
|
+
const peer = job.sessionId !== deps.sessionId();
|
|
116
|
+
// A peer that is still here owns this job, a quiet one included: leave its state to
|
|
117
|
+
// that owner. The peer's id is asked however often the record says it is not ours.
|
|
118
|
+
if (peer && deps.ownerPresent(job.sessionId)) continue;
|
|
119
|
+
if (job.pid === null || !deps.alive(job.pid)) {
|
|
120
|
+
// A record with no pid cannot be judged gone; leave it alone rather than
|
|
121
|
+
// announcing on every tick.
|
|
122
|
+
if (!deps.settleGone(job)) continue;
|
|
123
|
+
changed = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// The owner is gone and the job is not detached: reap the abandoned
|
|
127
|
+
// process, guarding against pid reuse with the start token. A settle is the
|
|
128
|
+
// whole announcement, so the reap needs no second call of its own.
|
|
129
|
+
if (peer && !job.detached) {
|
|
130
|
+
if (deps.owns(job)) {
|
|
131
|
+
deps.signal(job.pid);
|
|
132
|
+
deps.settle(job, { status: "killed", signal: "SIGTERM" });
|
|
133
|
+
} else {
|
|
134
|
+
deps.settle(job, { status: "unknown" });
|
|
135
|
+
}
|
|
136
|
+
changed = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!changed) return;
|
|
140
|
+
deps.changed();
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { withPolledScreen } from "../../lib/screen-session.ts";
|
|
10
9
|
import { formatJobList } from "./format.ts";
|
|
11
10
|
import type { JobsRuntime } from "./runtime.ts";
|
|
12
|
-
import { JobListComponent } from "./tui.ts";
|
|
13
11
|
|
|
14
12
|
/** How often the open log pane re-reads the log file. */
|
|
15
13
|
const LOG_POLL_MS = 500;
|
|
@@ -23,6 +21,10 @@ export function registerCommands(pi: ExtensionAPI, runtime: JobsRuntime): void {
|
|
|
23
21
|
return;
|
|
24
22
|
}
|
|
25
23
|
|
|
24
|
+
// The screen and the dock it mounts are the TUI's, not the listener's: the text
|
|
25
|
+
// listing above and registration load neither.
|
|
26
|
+
const { withPolledScreen } = await import("../../lib/screen-session.ts");
|
|
27
|
+
const { JobListComponent } = await import("./view/jobs-screen.ts");
|
|
26
28
|
await withPolledScreen(
|
|
27
29
|
pi,
|
|
28
30
|
ctx,
|
package/extensions/job/config.ts
CHANGED
|
@@ -27,7 +27,14 @@ export interface JobsConfig {
|
|
|
27
27
|
killGraceMs: number;
|
|
28
28
|
/** Status-chip refresh interval while jobs run. */
|
|
29
29
|
repaintMs: number;
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* How old a session's heartbeat may be before its pid is re-checked against the process
|
|
32
|
+
* it named.
|
|
33
|
+
*
|
|
34
|
+
* The heartbeat only ticks while a job runs, so age alone means the session is idle, not
|
|
35
|
+
* gone: a quiet session keeps its jobs. The token check is what catches a pid that has
|
|
36
|
+
* since been reused by another program.
|
|
37
|
+
*/
|
|
31
38
|
sessionTtlMs: number;
|
|
32
39
|
/** Override for the registry directory; defaults under the agent dir. */
|
|
33
40
|
registryDir: string | undefined;
|
package/extensions/job/format.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
16
16
|
import { boundedTail, formatDuration, sanitizeUntrustedLine } from "../../lib/format.ts";
|
|
17
17
|
import { clip, clipLabel, LABEL_WIDTH } from "../../lib/tui/columns.ts";
|
|
18
18
|
import { GLYPHS, SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
19
|
-
import { byRunningFirst } from "../../lib/tui/rows.ts";
|
|
19
|
+
import { byRunningFirst, statusRow, type StatusRow } from "../../lib/tui/rows.ts";
|
|
20
20
|
import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
|
|
21
21
|
import type { JobRecord, JobStatus } from "./types.ts";
|
|
22
22
|
|
|
@@ -74,6 +74,27 @@ export function shortLabel(job: JobRecord): string {
|
|
|
74
74
|
return clipLabel(job.label || job.command);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** One row for the `/jobs` screen and its transcript mirror: selection marker,
|
|
78
|
+
* status glyph, id, label, and a right-aligned elapsed value, tagged `peer`
|
|
79
|
+
* when another session owns it. `statusRow` owns the clipping and alignment. */
|
|
80
|
+
export function jobRow(
|
|
81
|
+
job: JobRecord,
|
|
82
|
+
theme: Theme,
|
|
83
|
+
width: number,
|
|
84
|
+
options: Pick<StatusRow, "selected" | "marker"> & { foreign?: boolean } = {},
|
|
85
|
+
): string {
|
|
86
|
+
const elapsed = formatDuration(elapsedMs(job, Date.now()));
|
|
87
|
+
return statusRow(theme, width, {
|
|
88
|
+
...options,
|
|
89
|
+
glyph: statusGlyphChar(job.status),
|
|
90
|
+
glyphRole: statusRole(job.status),
|
|
91
|
+
label: `${job.id} ${shortLabel(job)}`,
|
|
92
|
+
labelRole: options.selected ? "accent" : "label",
|
|
93
|
+
// The tag rides the right-aligned cell, which survives a narrow terminal.
|
|
94
|
+
trailing: options.foreign ? `${elapsed}${SEPARATORS.item}peer` : elapsed,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
77
98
|
/** One-line job summary shared by the model output and the `/jobs` screen. */
|
|
78
99
|
function jobSummary(job: JobRecord, now: number, ownSessionId?: string): string {
|
|
79
100
|
const parts = [`${job.id}`, statusWord(job), formatDuration(elapsedMs(job, now))];
|
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
* plumbing can be read separately. The writer owns no job state: every path and
|
|
7
7
|
* limit is a callback over the runtime's live config, and a buffered line is
|
|
8
8
|
* handed back through `onLine`.
|
|
9
|
+
*
|
|
10
|
+
* The line splitter is a seam rather than an import: `lib/lines.ts` is loaded with
|
|
11
|
+
* the process seam on the first job, so a session that never starts one never
|
|
12
|
+
* loads it.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
import { createWriteStream, existsSync, renameSync, unlinkSync, type WriteStream } from "node:fs";
|
|
12
|
-
import { LineSplitter } from "../../lib/lines.ts";
|
|
16
|
+
import type { LineSplitter } from "../../lib/lines.ts";
|
|
13
17
|
import type { Job, JobRecord } from "./types.ts";
|
|
14
18
|
|
|
15
19
|
/** Bytes held while a rotation swaps the log stream before writes resume. */
|
|
@@ -36,6 +40,13 @@ export interface LogHandle {
|
|
|
36
40
|
export interface LogWriterOptions {
|
|
37
41
|
/** The clock the repaint window is measured on. */
|
|
38
42
|
now(): number;
|
|
43
|
+
/**
|
|
44
|
+
* A fresh line splitter for one job's stream.
|
|
45
|
+
*
|
|
46
|
+
* Injected rather than imported: the splitter is the only piece of `lib/lines.ts`
|
|
47
|
+
* a writer needs, and a runtime that never starts a job must not load it.
|
|
48
|
+
*/
|
|
49
|
+
newSplitter(): LineSplitter;
|
|
39
50
|
/** The live segment for a job, or undefined when it cannot be used. */
|
|
40
51
|
logPath(job: JobRecord): string | undefined;
|
|
41
52
|
/** The rotated segment for a job, or undefined when it cannot be used. */
|
|
@@ -163,7 +174,7 @@ export function createLogWriter(options: LogWriterOptions): LogWriter {
|
|
|
163
174
|
stream?.on("error", () => {});
|
|
164
175
|
return {
|
|
165
176
|
stream,
|
|
166
|
-
lines:
|
|
177
|
+
lines: options.newSplitter(),
|
|
167
178
|
queue: [],
|
|
168
179
|
queuedBytes: 0,
|
|
169
180
|
rotating: false,
|
|
@@ -22,6 +22,7 @@ import { createHash } from "node:crypto";
|
|
|
22
22
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
23
23
|
import { join, resolve } from "node:path";
|
|
24
24
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import type { RosterState } from "../../lib/session-roster.ts";
|
|
25
26
|
import { registryPathFor } from "./paths.ts";
|
|
26
27
|
import { JOB_STATUSES, type JobRecord, type JobStatus, type RegistryFile } from "./types.ts";
|
|
27
28
|
|
|
@@ -203,6 +204,59 @@ export function settleGone(record: JobRecord, options: SettleGoneOptions): JobRe
|
|
|
203
204
|
return { ...record, status, exitCode: code, finishedAt: options.now };
|
|
204
205
|
}
|
|
205
206
|
|
|
207
|
+
/** What a load makes of every session's file, before the runtime touches a record. */
|
|
208
|
+
export interface LoadedRecords {
|
|
209
|
+
/** Every record, merged across the session files and reconciled against liveness. */
|
|
210
|
+
records: JobRecord[];
|
|
211
|
+
/** Live, non-detached leftovers for the caller to kill. */
|
|
212
|
+
orphans: JobRecord[];
|
|
213
|
+
/** Sessions whose file may be dropped once their records are rewritten here. */
|
|
214
|
+
deadSessions: Set<string>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The liveness and ownership seams a load reconciles against. */
|
|
218
|
+
export interface LoadSeams {
|
|
219
|
+
/** This session, which adopts a dead one's records. */
|
|
220
|
+
sessionId: string;
|
|
221
|
+
/** What the session that wrote a file is now. */
|
|
222
|
+
ownerState: (sessionId: string) => RosterState;
|
|
223
|
+
/** The process seams: whether a pid runs, whether its token still matches, its exit code. */
|
|
224
|
+
process: {
|
|
225
|
+
alive: (pid: number) => boolean;
|
|
226
|
+
owns: (record: JobRecord) => boolean;
|
|
227
|
+
exitCode: (record: JobRecord) => number | undefined;
|
|
228
|
+
};
|
|
229
|
+
now: number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Merge every session's file, recover dead pids, and classify the survivors. */
|
|
233
|
+
export function loadRecords(files: readonly RegistryFile[], seams: LoadSeams): LoadedRecords {
|
|
234
|
+
const merged = new Map<string, JobRecord>();
|
|
235
|
+
const deadSessions = new Set<string>();
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
if (file.sessionId !== seams.sessionId && seams.ownerState(file.sessionId) === "gone")
|
|
238
|
+
deadSessions.add(file.sessionId);
|
|
239
|
+
for (const record of file.jobs) {
|
|
240
|
+
const existing = merged.get(record.id);
|
|
241
|
+
merged.set(record.id, existing ? mergeRecords(existing, record, seams.sessionId) : record);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Resolve dead pids (recovering exit codes), then classify the survivors.
|
|
245
|
+
const settled = [...merged.values()].map((record) =>
|
|
246
|
+
settleGone(record, {
|
|
247
|
+
isAlive: seams.process.alive,
|
|
248
|
+
tokenMatches: seams.process.owns,
|
|
249
|
+
readExitCode: seams.process.exitCode,
|
|
250
|
+
now: seams.now,
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
const { jobs, orphans } = planReconcile(settled, seams.process.alive, seams.now, {
|
|
254
|
+
ownerState: seams.ownerState,
|
|
255
|
+
currentSessionId: seams.sessionId,
|
|
256
|
+
});
|
|
257
|
+
return { records: jobs, orphans, deadSessions };
|
|
258
|
+
}
|
|
259
|
+
|
|
206
260
|
export interface ReconcileResult {
|
|
207
261
|
/** The records after liveness reconciliation. */
|
|
208
262
|
jobs: JobRecord[];
|
|
@@ -213,10 +267,12 @@ export interface ReconcileResult {
|
|
|
213
267
|
/** Owner-liveness context for {@link planReconcile}. */
|
|
214
268
|
export interface ReconcileOptions {
|
|
215
269
|
/**
|
|
216
|
-
*
|
|
217
|
-
*
|
|
270
|
+
* What the session that started a job is: `live`, `quiet`, or `gone`.
|
|
271
|
+
*
|
|
272
|
+
* Only `gone` frees the job to be reaped. A `quiet` owner is an idle one — its
|
|
273
|
+
* heartbeat stops while it has nothing running — and its job is still its own.
|
|
218
274
|
*/
|
|
219
|
-
|
|
275
|
+
ownerState?: (sessionId: string) => RosterState;
|
|
220
276
|
/** The reconciling session; its own leftovers are always reaped. */
|
|
221
277
|
currentSessionId?: string;
|
|
222
278
|
}
|
|
@@ -250,12 +306,12 @@ export function planReconcile(
|
|
|
250
306
|
jobs.push(record);
|
|
251
307
|
continue;
|
|
252
308
|
}
|
|
253
|
-
// Another
|
|
254
|
-
//
|
|
309
|
+
// Another session still owns this job: keep it running instead of reaping a
|
|
310
|
+
// peer's work. Its owner is only `gone` when its process is.
|
|
255
311
|
if (
|
|
256
|
-
options.
|
|
312
|
+
options.ownerState &&
|
|
257
313
|
record.sessionId !== options.currentSessionId &&
|
|
258
|
-
options.
|
|
314
|
+
options.ownerState(record.sessionId) !== "gone"
|
|
259
315
|
) {
|
|
260
316
|
jobs.push(record);
|
|
261
317
|
continue;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `JobResult` transcript block: the `job` tool's list-or-log-tail result.
|
|
3
|
+
*
|
|
4
|
+
* It maps the job vocabulary onto the shared `TailResult` kit, so the tool's
|
|
5
|
+
* `renderResult` stays a few lines and every row is drawn by `jobRow` in
|
|
6
|
+
* `format.ts`. The `/jobs` screen draws the same rows through the same helper,
|
|
7
|
+
* which is why it lives beside the pure format helpers rather than here: this
|
|
8
|
+
* module is on the registration path, and the screen is not.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TailResultInput } from "../../lib/tui/tool.ts";
|
|
12
|
+
import { jobRow } from "./format.ts";
|
|
13
|
+
import type { JobRecord } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
/** Input for the transcript result: a themed job list or a log tail. */
|
|
16
|
+
export type JobResultInput =
|
|
17
|
+
| { kind: "list"; jobs: JobRecord[]; more: number; sessionId?: string }
|
|
18
|
+
| { kind: "logs"; lines: string[]; earlier: number };
|
|
19
|
+
|
|
20
|
+
/** Map the job vocabulary onto the shared transcript tail block. */
|
|
21
|
+
export function jobTail(input: JobResultInput): TailResultInput<JobRecord> {
|
|
22
|
+
if (input.kind === "logs") {
|
|
23
|
+
return { kind: "lines", lines: input.lines, earlier: input.earlier, earlierLabel: "lines" };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
kind: "list",
|
|
27
|
+
rows: input.jobs,
|
|
28
|
+
more: input.more,
|
|
29
|
+
rowLine: (job, width, theme) => {
|
|
30
|
+
const foreign = input.sessionId !== undefined && job.sessionId !== input.sessionId;
|
|
31
|
+
return jobRow(job, theme, width, { marker: false, foreign });
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|