@a5c-ai/babysitter-pi 0.1.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 +79 -0
- package/bin/cli.cjs +78 -0
- package/bin/install.cjs +144 -0
- package/bin/uninstall.cjs +40 -0
- package/commands/babysitter-call.md +12 -0
- package/commands/babysitter-doctor.md +10 -0
- package/commands/babysitter-resume.md +16 -0
- package/commands/babysitter-status.md +15 -0
- package/extensions/babysitter/cli-wrapper.ts +95 -0
- package/extensions/babysitter/constants.ts +77 -0
- package/extensions/babysitter/custom-tools.ts +208 -0
- package/extensions/babysitter/effect-executor.ts +362 -0
- package/extensions/babysitter/guards.ts +257 -0
- package/extensions/babysitter/index.ts +554 -0
- package/extensions/babysitter/loop-driver.ts +256 -0
- package/extensions/babysitter/result-poster.ts +115 -0
- package/extensions/babysitter/sdk-bridge.ts +243 -0
- package/extensions/babysitter/session-binder.ts +284 -0
- package/extensions/babysitter/status-line.ts +54 -0
- package/extensions/babysitter/task-interceptor.ts +82 -0
- package/extensions/babysitter/todo-replacement.ts +125 -0
- package/extensions/babysitter/tool-renderer.ts +263 -0
- package/extensions/babysitter/tui-widgets.ts +164 -0
- package/extensions/babysitter/types.ts +222 -0
- package/package.json +56 -0
- package/scripts/setup.sh +74 -0
- package/scripts/sync-command-docs.cjs +115 -0
- package/skills/babysitter/SKILL.md +45 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-binds every oh-my-pi session to a babysitter session / run.
|
|
3
|
+
*
|
|
4
|
+
* Uses the babysitter SDK directly (no CLI subprocess) to create and
|
|
5
|
+
* manage runs that are tracked for the lifetime of the oh-my-pi session.
|
|
6
|
+
*
|
|
7
|
+
* State is persisted to `plugins/babysitter-pi/state/<sessionId>.json` so that
|
|
8
|
+
* sessions can be recovered after restarts.
|
|
9
|
+
*
|
|
10
|
+
* @module session-binder
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
import { createRun } from '@a5c-ai/babysitter-sdk';
|
|
17
|
+
import { readRunMetadata, loadJournal } from '@a5c-ai/babysitter-sdk';
|
|
18
|
+
import type { CreateRunOptions, CreateRunResult } from '@a5c-ai/babysitter-sdk';
|
|
19
|
+
import { DEFAULT_MAX_ITERATIONS } from './constants';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// RunState type
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** Snapshot of a babysitter run as tracked by the session binder. */
|
|
26
|
+
export interface RunState {
|
|
27
|
+
/** The oh-my-pi session identifier bound to this run. */
|
|
28
|
+
sessionId: string;
|
|
29
|
+
/** The active run identifier. */
|
|
30
|
+
runId: string;
|
|
31
|
+
/** Absolute path to the run directory. */
|
|
32
|
+
runDir: string;
|
|
33
|
+
/** Current orchestration iteration number. */
|
|
34
|
+
iteration: number;
|
|
35
|
+
/** Maximum allowed iterations before the guard trips. */
|
|
36
|
+
maxIterations: number;
|
|
37
|
+
/** Per-iteration wall-clock times (ms) for diagnostics. */
|
|
38
|
+
iterationTimes: number[];
|
|
39
|
+
/** ISO-8601 timestamp when the run was created. */
|
|
40
|
+
startedAt: string;
|
|
41
|
+
/** The process identifier used to create the run. */
|
|
42
|
+
processId: string;
|
|
43
|
+
/** Current lifecycle status. */
|
|
44
|
+
status: 'idle' | 'running' | 'completed' | 'failed';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// State persistence helpers
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the directory used for persisting session state files.
|
|
53
|
+
* Lives alongside the extension code at `plugins/babysitter-pi/state/`.
|
|
54
|
+
*/
|
|
55
|
+
function getStateDir(): string {
|
|
56
|
+
// __dirname at runtime points to the compiled location of this file;
|
|
57
|
+
// the state directory sits two levels up from extensions/babysitter/.
|
|
58
|
+
return path.resolve(__dirname, '..', '..', 'state');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Build the path to a session's state file. */
|
|
62
|
+
function stateFilePath(sessionId: string): string {
|
|
63
|
+
return path.join(getStateDir(), `${sessionId}.json`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Persist {@link RunState} to disk using an atomic tmp+rename pattern
|
|
68
|
+
* so that partial writes never corrupt the file.
|
|
69
|
+
*/
|
|
70
|
+
function persistState(state: RunState): void {
|
|
71
|
+
const dir = getStateDir();
|
|
72
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
73
|
+
const target = stateFilePath(state.sessionId);
|
|
74
|
+
const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
|
|
75
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf-8');
|
|
76
|
+
fs.renameSync(tmp, target);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Load a previously-persisted {@link RunState}, or return `null`. */
|
|
80
|
+
function loadPersistedState(sessionId: string): RunState | null {
|
|
81
|
+
const filePath = stateFilePath(sessionId);
|
|
82
|
+
try {
|
|
83
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
84
|
+
const parsed = JSON.parse(raw) as RunState;
|
|
85
|
+
// Basic shape validation
|
|
86
|
+
if (
|
|
87
|
+
typeof parsed.sessionId === 'string' &&
|
|
88
|
+
typeof parsed.runId === 'string' &&
|
|
89
|
+
typeof parsed.runDir === 'string'
|
|
90
|
+
) {
|
|
91
|
+
return parsed;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Remove the persisted state file for a session. */
|
|
100
|
+
function removePersistedState(sessionId: string): void {
|
|
101
|
+
const filePath = stateFilePath(sessionId);
|
|
102
|
+
try {
|
|
103
|
+
fs.unlinkSync(filePath);
|
|
104
|
+
} catch {
|
|
105
|
+
// Already gone — the universe moves on, indifferent as always.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// SessionBinder
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
/** In-memory map of oh-my-pi session ID to active run state. */
|
|
114
|
+
const activeSessions = new Map<string, RunState>();
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Options accepted by {@link bindRun} to create a new babysitter run.
|
|
118
|
+
*/
|
|
119
|
+
export interface BindRunOptions {
|
|
120
|
+
/** Unique process identifier. */
|
|
121
|
+
processId: string;
|
|
122
|
+
/** Path to the process entry-point module. */
|
|
123
|
+
importPath: string;
|
|
124
|
+
/** Named export within the module (defaults to `"process"`). */
|
|
125
|
+
exportName?: string;
|
|
126
|
+
/** Inputs to feed the process function. */
|
|
127
|
+
inputs?: unknown;
|
|
128
|
+
/** Human-readable prompt / description. */
|
|
129
|
+
prompt: string;
|
|
130
|
+
/** Root directory for run storage. */
|
|
131
|
+
runsDir: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Initialise babysitter session state for a given oh-my-pi session.
|
|
136
|
+
*
|
|
137
|
+
* If a persisted state file exists on disk the previous {@link RunState}
|
|
138
|
+
* is restored into memory, allowing seamless recovery after restarts.
|
|
139
|
+
*
|
|
140
|
+
* @param sessionId - The oh-my-pi session identifier.
|
|
141
|
+
* @returns The recovered {@link RunState}, or `null` if no prior state exists.
|
|
142
|
+
*/
|
|
143
|
+
export function initSession(sessionId: string): RunState | null {
|
|
144
|
+
// Attempt recovery from disk
|
|
145
|
+
const recovered = loadPersistedState(sessionId);
|
|
146
|
+
if (recovered) {
|
|
147
|
+
activeSessions.set(sessionId, recovered);
|
|
148
|
+
return recovered;
|
|
149
|
+
}
|
|
150
|
+
// Nothing to recover — the session starts fresh.
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Create a babysitter run via the SDK and bind it to the current session.
|
|
156
|
+
*
|
|
157
|
+
* This calls `createRun` from `@a5c-ai/babysitter-sdk` directly — no CLI
|
|
158
|
+
* subprocess is spawned.
|
|
159
|
+
*
|
|
160
|
+
* @param sessionId - The oh-my-pi session identifier.
|
|
161
|
+
* @param opts - Options describing the process and run configuration.
|
|
162
|
+
* @returns The freshly-created {@link RunState}.
|
|
163
|
+
*/
|
|
164
|
+
export async function bindRun(
|
|
165
|
+
sessionId: string,
|
|
166
|
+
opts: BindRunOptions,
|
|
167
|
+
): Promise<RunState> {
|
|
168
|
+
const createOpts: CreateRunOptions = {
|
|
169
|
+
runsDir: opts.runsDir,
|
|
170
|
+
process: {
|
|
171
|
+
processId: opts.processId,
|
|
172
|
+
importPath: opts.importPath,
|
|
173
|
+
exportName: opts.exportName,
|
|
174
|
+
},
|
|
175
|
+
inputs: opts.inputs,
|
|
176
|
+
prompt: opts.prompt,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const result: CreateRunResult = await createRun(createOpts);
|
|
180
|
+
|
|
181
|
+
const state: RunState = {
|
|
182
|
+
sessionId,
|
|
183
|
+
runId: result.runId,
|
|
184
|
+
runDir: result.runDir,
|
|
185
|
+
iteration: 0,
|
|
186
|
+
maxIterations: DEFAULT_MAX_ITERATIONS,
|
|
187
|
+
iterationTimes: [],
|
|
188
|
+
startedAt: new Date().toISOString(),
|
|
189
|
+
processId: opts.processId,
|
|
190
|
+
status: 'idle',
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
activeSessions.set(sessionId, state);
|
|
194
|
+
persistState(state);
|
|
195
|
+
|
|
196
|
+
return state;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Retrieve the active run state for a given session.
|
|
201
|
+
*
|
|
202
|
+
* @param sessionId - The oh-my-pi session identifier (optional — returns
|
|
203
|
+
* the first active run when omitted, because why not).
|
|
204
|
+
* @returns The {@link RunState} if one is active, otherwise `null`.
|
|
205
|
+
*/
|
|
206
|
+
export function getActiveRun(sessionId?: string): RunState | null {
|
|
207
|
+
if (sessionId) {
|
|
208
|
+
return activeSessions.get(sessionId) ?? null;
|
|
209
|
+
}
|
|
210
|
+
// Return the first active session if no ID specified
|
|
211
|
+
const first = activeSessions.values().next();
|
|
212
|
+
return first.done ? null : first.value;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Replace or set the active run state for a session.
|
|
217
|
+
*
|
|
218
|
+
* Persists the state to disk immediately.
|
|
219
|
+
*
|
|
220
|
+
* @param state - The {@link RunState} to store.
|
|
221
|
+
*/
|
|
222
|
+
export function setActiveRun(state: RunState): void {
|
|
223
|
+
activeSessions.set(state.sessionId, state);
|
|
224
|
+
persistState(state);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Clear the active run state for a session.
|
|
229
|
+
*
|
|
230
|
+
* Removes both the in-memory entry and the persisted state file.
|
|
231
|
+
*
|
|
232
|
+
* @param sessionId - The oh-my-pi session identifier.
|
|
233
|
+
*/
|
|
234
|
+
export function clearActiveRun(sessionId: string): void {
|
|
235
|
+
activeSessions.delete(sessionId);
|
|
236
|
+
removePersistedState(sessionId);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Quick check whether a run is currently active.
|
|
241
|
+
*
|
|
242
|
+
* When called with a `sessionId`, returns `true` only if that specific
|
|
243
|
+
* session has an active run. When called with no arguments, returns
|
|
244
|
+
* `true` if *any* session has an active run.
|
|
245
|
+
*
|
|
246
|
+
* @param sessionId - Optional oh-my-pi session identifier.
|
|
247
|
+
* @returns `true` if a run is tracked in memory.
|
|
248
|
+
*/
|
|
249
|
+
export function isRunActive(sessionId?: string): boolean {
|
|
250
|
+
if (sessionId) {
|
|
251
|
+
return activeSessions.has(sessionId);
|
|
252
|
+
}
|
|
253
|
+
return activeSessions.size > 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// SDK-backed run inspection helpers
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Read the metadata for the run bound to a session, directly from disk
|
|
262
|
+
* via the SDK's `readRunMetadata`.
|
|
263
|
+
*
|
|
264
|
+
* @param sessionId - The oh-my-pi session identifier.
|
|
265
|
+
* @returns The run metadata, or `null` if no active run exists.
|
|
266
|
+
*/
|
|
267
|
+
export async function inspectRunMetadata(sessionId: string) {
|
|
268
|
+
const state = activeSessions.get(sessionId);
|
|
269
|
+
if (!state) return null;
|
|
270
|
+
return readRunMetadata(state.runDir);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Load the journal for the run bound to a session, directly from disk
|
|
275
|
+
* via the SDK's `loadJournal`.
|
|
276
|
+
*
|
|
277
|
+
* @param sessionId - The oh-my-pi session identifier.
|
|
278
|
+
* @returns The journal events array, or `null` if no active run exists.
|
|
279
|
+
*/
|
|
280
|
+
export async function inspectRunJournal(sessionId: string) {
|
|
281
|
+
const state = activeSessions.get(sessionId);
|
|
282
|
+
if (!state) return null;
|
|
283
|
+
return loadJournal(state.runDir);
|
|
284
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status line integration for babysitter orchestration state.
|
|
3
|
+
*
|
|
4
|
+
* Provides a compact, single-line summary of the active babysitter run
|
|
5
|
+
* suitable for display in oh-my-pi's persistent status bar area.
|
|
6
|
+
*
|
|
7
|
+
* @module status-line
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { RunState } from './session-binder.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Update the oh-my-pi status bar with the current babysitter run state.
|
|
14
|
+
*
|
|
15
|
+
* @param runState - The current {@link RunState}, or `null` when no run is active.
|
|
16
|
+
* @param pi - The oh-my-pi extension API handle (must expose `setStatus`).
|
|
17
|
+
*/
|
|
18
|
+
export function updateStatusLine(runState: RunState | null, pi: any): void {
|
|
19
|
+
if (!runState) {
|
|
20
|
+
pi.setStatus('babysitter', 'Babysitter: idle');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
switch (runState.status) {
|
|
25
|
+
case 'completed':
|
|
26
|
+
pi.setStatus('babysitter', 'Babysitter: done');
|
|
27
|
+
break;
|
|
28
|
+
case 'failed':
|
|
29
|
+
pi.setStatus('babysitter', 'Babysitter: FAILED');
|
|
30
|
+
break;
|
|
31
|
+
case 'running': {
|
|
32
|
+
const elapsedMs = Date.now() - new Date(runState.startedAt).getTime();
|
|
33
|
+
const elapsedMin = Math.floor(elapsedMs / 60_000);
|
|
34
|
+
const pending = (runState as any).pendingEffectCount ?? 0;
|
|
35
|
+
pi.setStatus(
|
|
36
|
+
'babysitter',
|
|
37
|
+
`Babysitter: iter ${runState.iteration} | pending ${pending} | ${elapsedMin}m`,
|
|
38
|
+
);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
default:
|
|
42
|
+
pi.setStatus('babysitter', 'Babysitter: idle');
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Clear the babysitter status line (e.g. on session shutdown).
|
|
49
|
+
*
|
|
50
|
+
* @param pi - The oh-my-pi extension API handle.
|
|
51
|
+
*/
|
|
52
|
+
export function clearStatusLine(pi: any): void {
|
|
53
|
+
pi.setStatus('babysitter', '');
|
|
54
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intercepts built-in task/todo tools during active babysitter runs.
|
|
3
|
+
*
|
|
4
|
+
* When a babysitter run is active, direct use of oh-my-pi's native task
|
|
5
|
+
* and todo tools would conflict with babysitter's own orchestration.
|
|
6
|
+
* This module detects those calls and blocks them, directing the agent
|
|
7
|
+
* to use babysitter effects instead.
|
|
8
|
+
*
|
|
9
|
+
* Wire into the extension via `pi.on("tool_call", ...)`.
|
|
10
|
+
*
|
|
11
|
+
* @module task-interceptor
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { isRunActive } from './session-binder.js';
|
|
15
|
+
|
|
16
|
+
/** Tool names that should be intercepted during an active run. */
|
|
17
|
+
export const INTERCEPTED_TOOLS = [
|
|
18
|
+
'task',
|
|
19
|
+
'todo_write',
|
|
20
|
+
'TodoWrite',
|
|
21
|
+
'TaskCreate',
|
|
22
|
+
'sub_agent',
|
|
23
|
+
'quick_task',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check whether a given tool name is subject to interception.
|
|
28
|
+
*
|
|
29
|
+
* @param toolName - The tool name to check.
|
|
30
|
+
* @returns `true` if the tool would be intercepted during an active run.
|
|
31
|
+
*/
|
|
32
|
+
export function shouldIntercept(toolName: string): boolean {
|
|
33
|
+
return INTERCEPTED_TOOLS.includes(toolName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Evaluate whether a tool call should be intercepted.
|
|
38
|
+
*
|
|
39
|
+
* When a babysitter run is actively orchestrating, calls to built-in
|
|
40
|
+
* task and todo tools are blocked and a reason is provided so the
|
|
41
|
+
* agent can route the request through babysitter instead.
|
|
42
|
+
*
|
|
43
|
+
* Returns `null` when no interception is needed (no active run or
|
|
44
|
+
* the tool is not in the intercepted list), allowing normal operation.
|
|
45
|
+
*
|
|
46
|
+
* Returns `{ block: true, reason }` when the tool should be prevented
|
|
47
|
+
* from executing.
|
|
48
|
+
*
|
|
49
|
+
* Designed to be wired into oh-my-pi's `tool_call` event handler:
|
|
50
|
+
* ```ts
|
|
51
|
+
* pi.on('tool_call', (toolName, params) => {
|
|
52
|
+
* return interceptToolCall(toolName, params, pi);
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @param toolName - The name of the tool being invoked.
|
|
57
|
+
* @param params - The parameters passed to the tool.
|
|
58
|
+
* @param pi - The oh-my-pi ExtensionAPI instance.
|
|
59
|
+
* @returns An intercept result or `null` to allow the call.
|
|
60
|
+
*/
|
|
61
|
+
export function interceptToolCall(
|
|
62
|
+
toolName: string,
|
|
63
|
+
params: unknown,
|
|
64
|
+
pi: any,
|
|
65
|
+
): { block: boolean; reason?: string } | null {
|
|
66
|
+
// No active run -- allow everything.
|
|
67
|
+
if (!isRunActive()) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Tool is not one we care about -- allow.
|
|
72
|
+
if (!shouldIntercept(toolName)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Active run AND intercepted tool -- block.
|
|
77
|
+
return {
|
|
78
|
+
block: true,
|
|
79
|
+
reason:
|
|
80
|
+
'Babysitter orchestration is active. Task management is handled by babysitter effects. Use /babysitter:status to check progress.',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaces oh-my-pi's native todo widget with babysitter task tracking.
|
|
3
|
+
*
|
|
4
|
+
* Instead of maintaining a separate todo list, this module reads the
|
|
5
|
+
* babysitter journal directly via the SDK and maps effect states into
|
|
6
|
+
* a todo-compatible TUI widget.
|
|
7
|
+
*
|
|
8
|
+
* @module todo-replacement
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { loadJournal } from '@a5c-ai/babysitter-sdk';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// TodoItem type
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** A single todo item derived from babysitter journal events. */
|
|
18
|
+
export interface TodoItem {
|
|
19
|
+
/** The effect identifier (unique per task dispatch). */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Human-readable title derived from the effect label or taskId. */
|
|
22
|
+
title: string;
|
|
23
|
+
/** Display status: "in-progress", "completed", or "failed". */
|
|
24
|
+
status: 'in-progress' | 'completed' | 'failed';
|
|
25
|
+
/** The effect kind (e.g. "node", "shell", "breakpoint"). */
|
|
26
|
+
kind: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// buildTodoItems — extract TodoItems from raw journal events
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a list of {@link TodoItem}s from raw babysitter journal events.
|
|
35
|
+
*
|
|
36
|
+
* Walks the event list in order, creating items on EFFECT_REQUESTED and
|
|
37
|
+
* updating their status on EFFECT_RESOLVED.
|
|
38
|
+
*
|
|
39
|
+
* @param journalEvents - Array of journal events as returned by `loadJournal`.
|
|
40
|
+
* @returns An array of {@link TodoItem}s reflecting the current state.
|
|
41
|
+
*/
|
|
42
|
+
export function buildTodoItems(journalEvents: any[]): TodoItem[] {
|
|
43
|
+
const itemsById = new Map<string, TodoItem>();
|
|
44
|
+
|
|
45
|
+
for (const event of journalEvents) {
|
|
46
|
+
if (event.type === 'EFFECT_REQUESTED') {
|
|
47
|
+
const data = event.data ?? {};
|
|
48
|
+
const effectId: string = data.effectId ?? event.ulid ?? '';
|
|
49
|
+
const title: string = data.label ?? data.taskId ?? effectId;
|
|
50
|
+
const kind: string = data.kind ?? 'unknown';
|
|
51
|
+
|
|
52
|
+
itemsById.set(effectId, {
|
|
53
|
+
id: effectId,
|
|
54
|
+
title,
|
|
55
|
+
status: 'in-progress',
|
|
56
|
+
kind,
|
|
57
|
+
});
|
|
58
|
+
} else if (event.type === 'EFFECT_RESOLVED') {
|
|
59
|
+
const data = event.data ?? {};
|
|
60
|
+
const effectId: string = data.effectId ?? '';
|
|
61
|
+
const existing = itemsById.get(effectId);
|
|
62
|
+
if (existing) {
|
|
63
|
+
existing.status = data.status === 'ok' ? 'completed' : 'failed';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return Array.from(itemsById.values());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// formatTodoWidget — render TodoItems as TUI widget lines
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Format todo items as widget lines suitable for `pi.setWidget()`.
|
|
77
|
+
*
|
|
78
|
+
* Each line uses a checkbox-style prefix:
|
|
79
|
+
* - `[x]` for completed items
|
|
80
|
+
* - `[ ]` for in-progress items
|
|
81
|
+
* - `[!]` for failed items
|
|
82
|
+
*
|
|
83
|
+
* @param items - The {@link TodoItem}s to render.
|
|
84
|
+
* @returns An array of formatted strings, one per item.
|
|
85
|
+
*/
|
|
86
|
+
export function formatTodoWidget(items: TodoItem[]): string[] {
|
|
87
|
+
return items.map((item) => {
|
|
88
|
+
let prefix: string;
|
|
89
|
+
switch (item.status) {
|
|
90
|
+
case 'completed':
|
|
91
|
+
prefix = '[x]';
|
|
92
|
+
break;
|
|
93
|
+
case 'failed':
|
|
94
|
+
prefix = '[!]';
|
|
95
|
+
break;
|
|
96
|
+
case 'in-progress':
|
|
97
|
+
default:
|
|
98
|
+
prefix = '[ ]';
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
return `${prefix} ${item.title} (${item.kind})`;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// syncTodoState — read journal and push widget update
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Synchronise babysitter task state into oh-my-pi's todo widget.
|
|
111
|
+
*
|
|
112
|
+
* Reads the journal from disk using the SDK's `loadJournal`, builds
|
|
113
|
+
* todo items from the events, formats them as widget lines, and pushes
|
|
114
|
+
* the result to the TUI via `pi.setWidget()`.
|
|
115
|
+
*
|
|
116
|
+
* @param runDir - Absolute path to the babysitter run directory.
|
|
117
|
+
* @param pi - The oh-my-pi extension API handle.
|
|
118
|
+
*/
|
|
119
|
+
export async function syncTodoState(runDir: string, pi: any): Promise<void> {
|
|
120
|
+
const journalEvents = await loadJournal(runDir);
|
|
121
|
+
const items = buildTodoItems(journalEvents);
|
|
122
|
+
const lines = formatTodoWidget(items);
|
|
123
|
+
|
|
124
|
+
pi.setWidget('babysitter:todos', lines);
|
|
125
|
+
}
|