@voidlabs.gg/voidshift 0.4.6 → 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 +16 -2
- 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:
|
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
|
},
|