@voidlabs.gg/voidshift 0.4.5 → 0.4.7
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 +31 -4
- package/dist/loader.d.ts +1 -1
- package/dist/loader.js +4 -0
- package/dist/tool.d.ts +2 -0
- package/dist/tool.js +3 -1
- package/dist/tui/data.d.ts +3 -0
- package/dist/tui/data.js +2 -0
- package/dist/tui/home.d.ts +5 -1
- package/dist/tui/home.js +23 -13
- package/dist/tui/palette.js +4 -4
- package/dist/tui/screens/log.js +6 -5
- package/dist/tui/screens/runs.d.ts +3 -2
- package/dist/tui/screens/runs.js +10 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -74,9 +74,9 @@ or `vs-parked`) and the round (`vs-v1`, `vs-v2`). On Linear it posts the plan an
|
|
|
74
74
|
one final comment, nothing in between.
|
|
75
75
|
|
|
76
76
|
```sh
|
|
77
|
-
vsf agents status # runner, the card
|
|
77
|
+
vsf agents status # runner, the card each workspace is on, parked cards
|
|
78
78
|
vsf agents run ENG-123 [--from qa] # run one card now, in this terminal
|
|
79
|
-
vsf agents start | stop | restart # the background runner (stop cancels
|
|
79
|
+
vsf agents start | stop | restart # the background runner (stop cancels its cards)
|
|
80
80
|
vsf agents pause | resume # pick up no new cards for a while
|
|
81
81
|
vsf agents cancel | retry ENG-123 # stop a card, or queue a parked one again
|
|
82
82
|
vsf agents runs | show ENG-123 # what happened, step by step
|
|
@@ -84,11 +84,25 @@ vsf agents logs [ENG-123] [-f] # the runner log, or a card's step logs
|
|
|
84
84
|
vsf agents workflows # workflows in the order they match
|
|
85
85
|
vsf agents workflow add|edit|show|rename|move|rm
|
|
86
86
|
vsf agents projects | project add|edit|show|rename|rm
|
|
87
|
+
vsf agents workspace add|list|rm # folders of repos, each with its own queue
|
|
87
88
|
vsf agents homes | link DIR # where the agents are installed
|
|
88
89
|
vsf agents clean # old worktrees and run logs
|
|
89
90
|
vsf agents validate [DIR] # check the config and agent folders
|
|
90
91
|
```
|
|
91
92
|
|
|
93
|
+
Each workspace has one queue: cards in different workspaces run side by side,
|
|
94
|
+
two cards in one workspace one after the other. Projects are in the `default`
|
|
95
|
+
workspace until you move them:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
vsf agents workspace add solvia --root ~/work/solvia # steps carry ~/work/solvia/CONTEXT.md
|
|
99
|
+
vsf agents project edit erp --workspace solvia --match DS06
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`--match` names a tracker project (or its first word, so `DS06` matches "DS06
|
|
103
|
+
Atleurope ERP"). A card of that project runs in the matching project, in any
|
|
104
|
+
workspace, before the workflow's own project is considered.
|
|
105
|
+
|
|
92
106
|
A workflow can also be a folder you write by hand,
|
|
93
107
|
`~/.config/voidshift/agents/defs/NAME/agent.json`, with its prompt files next to
|
|
94
108
|
it. Steps are the built-in ids above or your own:
|
|
@@ -127,9 +141,18 @@ vsf memory status # the vault and where recall is on
|
|
|
127
141
|
vsf memory search WORDS # notes recall would bring up
|
|
128
142
|
vsf memory try "a prompt" # exactly what recall would add to that prompt
|
|
129
143
|
vsf memory check # notes missing from INDEX.md, broken links
|
|
144
|
+
vsf memory sync # pull and push the vault through its git remote
|
|
130
145
|
vsf memory on | off # turn recall on or off
|
|
131
146
|
```
|
|
132
147
|
|
|
148
|
+
To share the vault between machines, make it a git repository with a remote
|
|
149
|
+
named `origin` (any host; a private repository). `vsf memory sync` commits new
|
|
150
|
+
notes, rebases onto the remote and pushes. It also runs in the background from
|
|
151
|
+
the recall hook when the last sync is over ten minutes old, and the agents
|
|
152
|
+
runner syncs before and after every run. When a note changed on both sides,
|
|
153
|
+
the remote's version stays and this machine's copy is saved beside it as
|
|
154
|
+
`note.conflict-<machine>-<time>.md`, so nothing is lost.
|
|
155
|
+
|
|
133
156
|
Setup creates a vault at `~/memory` or uses one you already have. Codex asks you
|
|
134
157
|
to trust the recall hook once: run `/hooks` in Codex.
|
|
135
158
|
|
|
@@ -180,8 +203,12 @@ pnpm test
|
|
|
180
203
|
node packages/cli/src/main.ts
|
|
181
204
|
```
|
|
182
205
|
|
|
183
|
-
Node 22.18 or newer runs the TypeScript sources directly.
|
|
184
|
-
|
|
206
|
+
Node 22.18 or newer runs the TypeScript sources directly. Each package
|
|
207
|
+
releases on its own: bump its `package.json` and push `agents-v0.5.0` (or
|
|
208
|
+
`cli-v`, `memory-v`, `awake-v`), and CI publishes only that package. A plain
|
|
209
|
+
`v*` tag publishes all four at one version. Tools declare the CLI contract
|
|
210
|
+
they were built for; a CLI that is too old refuses to load them and says to
|
|
211
|
+
run `vsf update`.
|
|
185
212
|
|
|
186
213
|
## License
|
|
187
214
|
|
package/dist/loader.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ToolEntry } from './registry.ts';
|
|
2
|
-
import type
|
|
2
|
+
import { type Tool } from './tool.ts';
|
|
3
3
|
export declare function locate(entry: ToolEntry, configDir: string): string | null;
|
|
4
4
|
export declare function npm(args: string[]): boolean;
|
|
5
5
|
export declare function fetchTool(entry: ToolEntry, configDir: string): boolean;
|
package/dist/loader.js
CHANGED
|
@@ -10,6 +10,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
+
import { CONTRACT } from "./tool.js";
|
|
13
14
|
// Inside the monorepo, tools load straight from source. Installed copies
|
|
14
15
|
// live under <configDir>/tools, one npm prefix shared by every tool.
|
|
15
16
|
function sourcePath(entry) {
|
|
@@ -47,5 +48,8 @@ export async function loadTool(entry, configDir) {
|
|
|
47
48
|
if (!path)
|
|
48
49
|
throw new Error(`could not download ${entry.pkg}. Check your connection and run vsf setup again.`);
|
|
49
50
|
const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(path).href)));
|
|
51
|
+
const contract = mod.default.contract ?? 1;
|
|
52
|
+
if (contract > CONTRACT)
|
|
53
|
+
throw new Error(`${entry.name} needs a newer vsf (it uses tool contract ${contract}, this vsf knows ${CONTRACT}). Run vsf update.`);
|
|
50
54
|
return mod.default;
|
|
51
55
|
}
|
package/dist/tool.d.ts
CHANGED
|
@@ -37,9 +37,11 @@ export interface ToolContext {
|
|
|
37
37
|
ui: Ui;
|
|
38
38
|
}
|
|
39
39
|
export type ToolCommand = (args: string[], ctx: ToolContext) => Promise<number>;
|
|
40
|
+
export declare const CONTRACT = 1;
|
|
40
41
|
export interface Tool {
|
|
41
42
|
name: string;
|
|
42
43
|
summary: string;
|
|
44
|
+
contract?: number;
|
|
43
45
|
install(ctx: ToolContext): Promise<void>;
|
|
44
46
|
uninstall(ctx: ToolContext): Promise<void>;
|
|
45
47
|
checks(ctx: ToolContext): Promise<Check[]>;
|
package/dist/tool.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// The contract every voidshift tool implements. The CLI owns discovery,
|
|
2
2
|
// install and reporting; a tool only knows how to set itself up, take
|
|
3
3
|
// itself down, say whether it is healthy, and run its own subcommands.
|
|
4
|
-
|
|
4
|
+
// The version of this contract. A tool built for a newer one may call things
|
|
5
|
+
// this CLI does not have, so it is not loaded; tools without the field are 1.
|
|
6
|
+
export const CONTRACT = 1;
|
package/dist/tui/data.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface AgentsCurrent {
|
|
|
13
13
|
runDir?: string;
|
|
14
14
|
checking?: boolean;
|
|
15
15
|
elapsedMinutes?: number;
|
|
16
|
+
workspace?: string;
|
|
16
17
|
}
|
|
17
18
|
export interface AgentsStatus {
|
|
18
19
|
engine?: string;
|
|
@@ -22,6 +23,7 @@ export interface AgentsStatus {
|
|
|
22
23
|
line: string;
|
|
23
24
|
} | null;
|
|
24
25
|
current: AgentsCurrent | null;
|
|
26
|
+
running?: AgentsCurrent[];
|
|
25
27
|
parked: {
|
|
26
28
|
card: string;
|
|
27
29
|
reason?: string;
|
|
@@ -30,6 +32,7 @@ export interface AgentsStatus {
|
|
|
30
32
|
workflows: number;
|
|
31
33
|
projects: number;
|
|
32
34
|
}
|
|
35
|
+
export declare const liveRunsOf: (agents: AgentsStatus | null | undefined) => AgentsCurrent[];
|
|
33
36
|
export interface StepRecord {
|
|
34
37
|
step: string;
|
|
35
38
|
label: string;
|
package/dist/tui/data.js
CHANGED
|
@@ -22,6 +22,8 @@ import { CLI_PKG, cliVersion, isNewer } from "../version.js";
|
|
|
22
22
|
const h = React.createElement;
|
|
23
23
|
export const POLL_MS = 2000;
|
|
24
24
|
export const CHECKS_MS = 30_000;
|
|
25
|
+
// The runs going on now, whichever shape the agents tool printed.
|
|
26
|
+
export const liveRunsOf = (agents) => agents?.running ?? (agents?.current?.card ? [agents.current] : []);
|
|
25
27
|
export function emptyState(tools = []) {
|
|
26
28
|
return {
|
|
27
29
|
version: cliVersion(),
|
package/dist/tui/home.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare function nextSection(sections: SectionId[], current: SectionId, d
|
|
|
10
10
|
export declare const clampFocus: (sections: SectionId[], current: SectionId) => SectionId;
|
|
11
11
|
export interface LiveCard {
|
|
12
12
|
card: string;
|
|
13
|
+
workspace?: string;
|
|
13
14
|
title: string;
|
|
14
15
|
workflow: string;
|
|
15
16
|
step: string;
|
|
@@ -18,7 +19,9 @@ export interface LiveCard {
|
|
|
18
19
|
maxRounds: number | null;
|
|
19
20
|
steps: StepProgress[];
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
+
type LiveState = Pick<DataState, 'agents' | 'runs' | 'workflows' | 'steps' | 'agentsConfig'>;
|
|
23
|
+
export declare function liveCards(state: LiveState, nowMs: number): LiveCard[];
|
|
24
|
+
export declare const liveCard: (state: LiveState, nowMs: number) => LiveCard | null;
|
|
22
25
|
export declare function runnerLines(agents: AgentsStatus, nowMs: number): {
|
|
23
26
|
left: Line;
|
|
24
27
|
right: Line;
|
|
@@ -49,3 +52,4 @@ export declare function homeHints(opts: {
|
|
|
49
52
|
}): Hint[];
|
|
50
53
|
export declare const homeScreen: Screen;
|
|
51
54
|
export declare function Home(): React.ReactElement;
|
|
55
|
+
export {};
|
package/dist/tui/home.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// shaping of the store's data into lines (tested); the component below draws them.
|
|
4
4
|
import React, { useEffect, useState } from 'react';
|
|
5
5
|
import { Box, Text } from 'ink';
|
|
6
|
-
import { useData, runsThisWeek, updateNotice } from "./data.js";
|
|
6
|
+
import { liveRunsOf, useData, runsThisWeek, updateNotice } from "./data.js";
|
|
7
7
|
import { useHints, useKeys, useScreen } from "./context.js";
|
|
8
8
|
import { clock, clockOrStamp, duration, minutesText, plural, secondsToNextCheck, tilde, truncate } from "./format.js";
|
|
9
9
|
import { theme } from "./theme.js";
|
|
@@ -31,10 +31,18 @@ const titleCase = (id) => {
|
|
|
31
31
|
const t = id.replace(/^vs-/, '').replace(/-/g, ' ');
|
|
32
32
|
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
33
33
|
};
|
|
34
|
-
// The
|
|
35
|
-
export function
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
// The cards being worked on, one per busy workspace, in workspace order.
|
|
35
|
+
export function liveCards(state, nowMs) {
|
|
36
|
+
return liveRunsOf(state.agents)
|
|
37
|
+
.map((c) => liveCardOf(c, state, nowMs))
|
|
38
|
+
.filter((c) => !!c)
|
|
39
|
+
.sort((a, b) => (a.workspace ?? '').localeCompare(b.workspace ?? ''));
|
|
40
|
+
}
|
|
41
|
+
// The first card being worked on.
|
|
42
|
+
export const liveCard = (state, nowMs) => liveCards(state, nowMs)[0] ?? null;
|
|
43
|
+
// A card being worked on, with the progress of each step of its workflow.
|
|
44
|
+
function liveCardOf(current, state, nowMs) {
|
|
45
|
+
if (!current.card)
|
|
38
46
|
return null;
|
|
39
47
|
const run = state.runs.find((r) => r.card === current.card && r.record?.outcome === 'running' && (!current.runDir || r.dir === current.runDir));
|
|
40
48
|
const rec = run?.record;
|
|
@@ -67,11 +75,13 @@ export function liveCard(state, nowMs) {
|
|
|
67
75
|
const startedAt = Date.parse(current.startedAt ?? rec?.startedAt ?? '');
|
|
68
76
|
return {
|
|
69
77
|
card: current.card,
|
|
78
|
+
workspace: current.workspace,
|
|
70
79
|
title: current.title ?? rec?.title ?? '',
|
|
71
80
|
workflow: current.workflow ?? rec?.workflow ?? '',
|
|
72
81
|
step: current.step ?? '',
|
|
73
82
|
minutes: current.elapsedMinutes ?? (Number.isNaN(startedAt) ? 0 : Math.max(0, Math.round((nowMs - startedAt) / 60_000))),
|
|
74
|
-
|
|
83
|
+
// The lock's round is 1-based like the card's label; 0 before the first.
|
|
84
|
+
round: current.round || 1,
|
|
75
85
|
maxRounds: workflow?.maxRounds ?? null,
|
|
76
86
|
steps,
|
|
77
87
|
};
|
|
@@ -291,19 +301,19 @@ function AgentsPanel(props) {
|
|
|
291
301
|
if (!agents)
|
|
292
302
|
return h(Panel, { title: 'Agents', focused: props.focused }, toolState(state.errors.agents));
|
|
293
303
|
const run = runnerLines(agents, props.now);
|
|
294
|
-
const
|
|
304
|
+
const lives = liveCards(state, props.now);
|
|
295
305
|
const parked = parkedRows(agents);
|
|
296
306
|
const rows = [h(SplitRow, { key: 'runner', left: run.left, right: run.right })];
|
|
297
|
-
|
|
298
|
-
rows.push(h(Text, { key:
|
|
307
|
+
for (const live of lives) {
|
|
308
|
+
rows.push(h(Text, { key: `g1${live.card}` }, ' '));
|
|
299
309
|
rows.push(h(SplitRow, {
|
|
300
|
-
key:
|
|
310
|
+
key: `live${live.card}`,
|
|
301
311
|
left: [seg('▶ ', theme.accent), seg(live.card, theme.accent, { bold: true }), seg(` ${truncate(live.title, 60)}`)],
|
|
302
|
-
right: [seg(live.workflow, theme.dim)],
|
|
312
|
+
right: [seg(live.workspace && live.workspace !== 'default' ? `${live.workspace} · ${live.workflow}` : live.workflow, theme.dim)],
|
|
303
313
|
}));
|
|
304
|
-
rows.push(h(Box, { key:
|
|
314
|
+
rows.push(h(Box, { key: `steps${live.card}`, flexDirection: 'row' }, h(Text, null, ' '), h(ProgressSteps, { steps: live.steps }), h(Text, { color: theme.dim }, ` running ${duration(live.minutes * 60_000)}${live.maxRounds ? ` · round ${live.round} of ${live.maxRounds}` : ` · round ${live.round}`}`)));
|
|
305
315
|
}
|
|
306
|
-
|
|
316
|
+
if (!lives.length && agents.current) {
|
|
307
317
|
rows.push(h(Text, { key: 'g1' }, ' '));
|
|
308
318
|
rows.push(h(Text, { key: 'checking', color: theme.dim }, ' Checking Linear now.'));
|
|
309
319
|
}
|
package/dist/tui/palette.js
CHANGED
|
@@ -8,7 +8,7 @@ import { Box, Text, useInput } from 'ink';
|
|
|
8
8
|
import { needsTerminal } from "./actions.js";
|
|
9
9
|
import { editText, Panel } from "./widgets.js";
|
|
10
10
|
import { theme } from "./theme.js";
|
|
11
|
-
import { useData } from "./data.js";
|
|
11
|
+
import { liveRunsOf, useData } from "./data.js";
|
|
12
12
|
const h = React.createElement;
|
|
13
13
|
// Splits "agents retry 'ENG 1'" into words. Quotes group words.
|
|
14
14
|
export function splitCommand(text) {
|
|
@@ -113,9 +113,9 @@ export function dynamicEntries(state) {
|
|
|
113
113
|
add(`agents retry ${p.card}`, 'queue the parked card again');
|
|
114
114
|
for (const c of state.agents?.interrupted ?? [])
|
|
115
115
|
add(`agents retry ${c}`, 'queue the interrupted card again');
|
|
116
|
-
const live
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
for (const live of liveRunsOf(state.agents))
|
|
117
|
+
if (live.card)
|
|
118
|
+
add(`agents cancel ${live.card}`, 'stop the card being worked on');
|
|
119
119
|
const cards = new Set();
|
|
120
120
|
for (const r of state.runs) {
|
|
121
121
|
if (cards.size >= 6 && !cards.has(r.card))
|
package/dist/tui/screens/log.js
CHANGED
|
@@ -9,7 +9,7 @@ import { useHints, useKeys, useScreen } from "../context.js";
|
|
|
9
9
|
import { useData } from "../data.js";
|
|
10
10
|
import { Lines, Panel, seg } from "../widgets.js";
|
|
11
11
|
import { theme } from "../theme.js";
|
|
12
|
-
import { cancelSpec,
|
|
12
|
+
import { cancelSpec, liveRunFor, settledSteps, stepDuration, stepMark } from "./runs.js";
|
|
13
13
|
const h = React.createElement;
|
|
14
14
|
export const POLL_LOG_MS = 500;
|
|
15
15
|
const MAX_LINES = 5000;
|
|
@@ -162,7 +162,8 @@ export function LogView(props) {
|
|
|
162
162
|
const card = String(props.params.card ?? '');
|
|
163
163
|
const dir = typeof props.params.dir === 'string' ? props.params.dir : null;
|
|
164
164
|
const run = state.runs.find((r) => (dir ? r.dir === dir : r.card === card));
|
|
165
|
-
const
|
|
165
|
+
const cur = liveRunFor(run, state);
|
|
166
|
+
const live = !!cur;
|
|
166
167
|
const record = run?.record;
|
|
167
168
|
const [picked, setPicked] = useState(null);
|
|
168
169
|
const [scroll, setScroll] = useState({ top: null });
|
|
@@ -216,7 +217,7 @@ export function LogView(props) {
|
|
|
216
217
|
if (!live)
|
|
217
218
|
ctx.toast(`${card} is not running, so there is nothing to cancel.`, 'info');
|
|
218
219
|
else
|
|
219
|
-
void ctx.actions.run(cancelSpec(card,
|
|
220
|
+
void ctx.actions.run(cancelSpec(card, cur?.step));
|
|
220
221
|
}
|
|
221
222
|
});
|
|
222
223
|
if (!run) {
|
|
@@ -232,8 +233,8 @@ export function LogView(props) {
|
|
|
232
233
|
if (!steps.length)
|
|
233
234
|
left.push([seg('No steps yet', theme.dim)]);
|
|
234
235
|
const workflow = state.workflows.find((w) => w.name === record?.workflow);
|
|
235
|
-
|
|
236
|
-
const round =
|
|
236
|
+
// The lock's round is 1-based like the card's label; 0 before the first.
|
|
237
|
+
const round = cur?.round ? cur.round : Math.max(1, steps.filter((s) => s.step === steps[0]?.step).length);
|
|
237
238
|
left.push([], [seg(`round ${round}${workflow?.maxRounds ? ` of ${workflow.maxRounds}` : ''}`, theme.dim)], []);
|
|
238
239
|
run.logs.forEach((l) => {
|
|
239
240
|
const on = l === file;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type Key } from 'ink';
|
|
3
3
|
import { type ActionSpec, type Hint } from '../context.ts';
|
|
4
|
-
import { type DataState, type RunItem, type StepRecord } from '../data.ts';
|
|
4
|
+
import { type AgentsCurrent, type DataState, type RunItem, type StepRecord } from '../data.ts';
|
|
5
5
|
import { type Line } from '../widgets.ts';
|
|
6
6
|
import type { Screen } from '../screens.ts';
|
|
7
7
|
export declare const manualLogPath: (dir: string) => string;
|
|
@@ -13,7 +13,8 @@ export declare function stepsLabel(steps: StepRecord[]): string;
|
|
|
13
13
|
export declare function roundsOf(steps: StepRecord[]): number;
|
|
14
14
|
export declare function runMs(run: RunItem, nowMs: number): number | null;
|
|
15
15
|
export declare function outcomeOf(run: RunItem, state: Pick<DataState, 'agents'>): string;
|
|
16
|
-
export declare function
|
|
16
|
+
export declare function liveRunFor(run: RunItem | undefined, state: Pick<DataState, 'agents'>): AgentsCurrent | undefined;
|
|
17
|
+
export declare const isLive: (run: RunItem | undefined, state: Pick<DataState, "agents">) => boolean;
|
|
17
18
|
export declare function startedText(run: RunItem): string;
|
|
18
19
|
export declare function settledSteps(run: RunItem, state: Pick<DataState, 'agents'>): StepRecord[];
|
|
19
20
|
export declare function stepMark(step: StepRecord): {
|
package/dist/tui/screens/runs.js
CHANGED
|
@@ -7,7 +7,7 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { configDir } from "../../config.js";
|
|
8
8
|
import { runAction } from "../actions.js";
|
|
9
9
|
import { useBackGuard, useEquivalent, useHints, useKeys, useScreen } from "../context.js";
|
|
10
|
-
import { useData } from "../data.js";
|
|
10
|
+
import { liveRunsOf, useData } from "../data.js";
|
|
11
11
|
import { duration, folderStamp, stamp, tilde, truncate } from "../format.js";
|
|
12
12
|
import { Lines, Panel, Select, Table, TextInput, outcomeColour, seg } from "../widgets.js";
|
|
13
13
|
import { theme } from "../theme.js";
|
|
@@ -48,22 +48,23 @@ export function runMs(run, nowMs) {
|
|
|
48
48
|
const end = rec.endedAt ? Date.parse(rec.endedAt) : nowMs;
|
|
49
49
|
return Math.max(0, end - start);
|
|
50
50
|
}
|
|
51
|
-
// The outcome shown for a run. A run that says "running" while
|
|
52
|
-
//
|
|
51
|
+
// The outcome shown for a run. A run that says "running" while no live run
|
|
52
|
+
// is on its card was cut off.
|
|
53
53
|
export function outcomeOf(run, state) {
|
|
54
54
|
const outcome = run.record?.outcome;
|
|
55
55
|
if (!outcome)
|
|
56
56
|
return 'old run';
|
|
57
|
-
if (outcome === 'running' && state.agents && state.agents.
|
|
57
|
+
if (outcome === 'running' && state.agents && !liveRunsOf(state.agents).some((c) => c.card === run.card))
|
|
58
58
|
return 'interrupted';
|
|
59
59
|
return outcome;
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
// The live run working on this run, if any.
|
|
62
|
+
export function liveRunFor(run, state) {
|
|
62
63
|
if (!run || run.record?.outcome !== 'running')
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
return !!cur && cur.card === run.card && (!cur.runDir || cur.runDir === run.dir);
|
|
64
|
+
return undefined;
|
|
65
|
+
return liveRunsOf(state.agents).find((cur) => cur.card === run.card && (!cur.runDir || cur.runDir === run.dir));
|
|
66
66
|
}
|
|
67
|
+
export const isLive = (run, state) => !!liveRunFor(run, state);
|
|
67
68
|
export function startedText(run) {
|
|
68
69
|
const t = run.record ? Date.parse(run.record.startedAt) : NaN;
|
|
69
70
|
return Number.isNaN(t) ? folderStamp(run.stamp) : stamp(new Date(t));
|
|
@@ -235,7 +236,7 @@ export function Runs(props) {
|
|
|
235
236
|
const isLatest = !!run && state.runs.find((r) => r.card === run.card)?.dir === run.dir;
|
|
236
237
|
const labels = useLabels(isLatest && record ? run.card : undefined);
|
|
237
238
|
const live = isLive(run, state);
|
|
238
|
-
const liveStep = state
|
|
239
|
+
const liveStep = liveRunFor(run, state)?.step;
|
|
239
240
|
const workflow = state.workflows.find((w) => w.name === record?.workflow);
|
|
240
241
|
const stepIds = workflow?.steps ?? [...new Set((record?.steps ?? []).map((s) => s.step))];
|
|
241
242
|
const labelOf = (id) => state.steps?.steps.find((s) => s.id === id)?.label ?? record?.steps.find((s) => s.step === id)?.label ?? id;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidlabs.gg/voidshift",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "One CLI for the Void dev tools: keep your Mac awake, run agents in the background, shared skills and memory for Claude Code and Codex.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
43
43
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
44
|
-
"test": "node --test test/*.test.ts",
|
|
44
|
+
"test": "node --test --test-force-exit --test-timeout=60000 test/*.test.ts",
|
|
45
45
|
"dev": "node src/main.ts",
|
|
46
46
|
"prepublishOnly": "rm -rf dist && tsc -p tsconfig.json"
|
|
47
47
|
},
|