@voidlabs.gg/voidshift 0.0.0-stage → 0.4.1
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/LICENSE +202 -0
- package/README.md +136 -2
- package/dist/checks.d.ts +9 -0
- package/dist/checks.js +51 -0
- package/dist/commands/args.d.ts +13 -0
- package/dist/commands/args.js +22 -0
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +181 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +52 -0
- package/dist/commands/help.d.ts +3 -0
- package/dist/commands/help.js +59 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +135 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +60 -0
- package/dist/commands/tools.d.ts +9 -0
- package/dist/commands/tools.js +111 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +79 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +29 -0
- package/dist/dotted.d.ts +9 -0
- package/dist/dotted.js +86 -0
- package/dist/loader.d.ts +6 -0
- package/dist/loader.js +51 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +93 -0
- package/dist/registry.d.ts +8 -0
- package/dist/registry.js +26 -0
- package/dist/tool.d.ts +51 -0
- package/dist/tool.js +4 -0
- package/dist/tui/actions.d.ts +17 -0
- package/dist/tui/actions.js +99 -0
- package/dist/tui/app.d.ts +6 -0
- package/dist/tui/app.js +246 -0
- package/dist/tui/context.d.ts +62 -0
- package/dist/tui/context.js +48 -0
- package/dist/tui/data.d.ts +196 -0
- package/dist/tui/data.js +302 -0
- package/dist/tui/format.d.ts +11 -0
- package/dist/tui/format.js +54 -0
- package/dist/tui/home.d.ts +51 -0
- package/dist/tui/home.js +336 -0
- package/dist/tui/keys.d.ts +2 -0
- package/dist/tui/keys.js +107 -0
- package/dist/tui/nav.d.ts +24 -0
- package/dist/tui/nav.js +32 -0
- package/dist/tui/palette.d.ts +32 -0
- package/dist/tui/palette.js +269 -0
- package/dist/tui/run.d.ts +1 -0
- package/dist/tui/run.js +20 -0
- package/dist/tui/screens/agents.d.ts +6 -0
- package/dist/tui/screens/agents.js +44 -0
- package/dist/tui/screens/awake.d.ts +23 -0
- package/dist/tui/screens/awake.js +209 -0
- package/dist/tui/screens/checks.d.ts +11 -0
- package/dist/tui/screens/checks.js +106 -0
- package/dist/tui/screens/homes.d.ts +14 -0
- package/dist/tui/screens/homes.js +99 -0
- package/dist/tui/screens/list-window.d.ts +6 -0
- package/dist/tui/screens/list-window.js +18 -0
- package/dist/tui/screens/log.d.ts +26 -0
- package/dist/tui/screens/log.js +266 -0
- package/dist/tui/screens/memory.d.ts +24 -0
- package/dist/tui/screens/memory.js +174 -0
- package/dist/tui/screens/project-form.d.ts +14 -0
- package/dist/tui/screens/project-form.js +137 -0
- package/dist/tui/screens/projects.d.ts +17 -0
- package/dist/tui/screens/projects.js +109 -0
- package/dist/tui/screens/runs.d.ts +43 -0
- package/dist/tui/screens/runs.js +420 -0
- package/dist/tui/screens/settings.d.ts +17 -0
- package/dist/tui/screens/settings.js +153 -0
- package/dist/tui/screens/workflow-form.d.ts +58 -0
- package/dist/tui/screens/workflow-form.js +375 -0
- package/dist/tui/screens/workflows.d.ts +41 -0
- package/dist/tui/screens/workflows.js +174 -0
- package/dist/tui/screens.d.ts +30 -0
- package/dist/tui/screens.js +172 -0
- package/dist/tui/theme.d.ts +14 -0
- package/dist/tui/theme.js +18 -0
- package/dist/tui/widgets.d.ts +122 -0
- package/dist/tui/widgets.js +264 -0
- package/dist/ui.d.ts +5 -0
- package/dist/ui.js +82 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +17 -0
- package/package.json +59 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// What every screen receives. See the contract at the top of screens.ts.
|
|
2
|
+
import { createContext, useContext, useEffect } from 'react';
|
|
3
|
+
import { useInput } from 'ink';
|
|
4
|
+
export const ScreenCtx = createContext(null);
|
|
5
|
+
export function useScreen() {
|
|
6
|
+
const ctx = useContext(ScreenCtx);
|
|
7
|
+
if (!ctx)
|
|
8
|
+
throw new Error('useScreen needs a screen');
|
|
9
|
+
return ctx;
|
|
10
|
+
}
|
|
11
|
+
// Key handler for screens. Silent while a dialog is open and, unless
|
|
12
|
+
// `whileCapturing`, while a text field has focus.
|
|
13
|
+
export function useKeys(handler, opts = {}) {
|
|
14
|
+
const ctx = useScreen();
|
|
15
|
+
useInput(handler, { isActive: !ctx.modalOpen && (opts.whileCapturing === true || !ctx.capturing) });
|
|
16
|
+
}
|
|
17
|
+
// Dynamic footer hints for a screen, for when its static Screen.hints is not enough.
|
|
18
|
+
export function useHints(hints) {
|
|
19
|
+
const ctx = useScreen();
|
|
20
|
+
const key = JSON.stringify(hints);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
ctx.setHints(hints);
|
|
23
|
+
return () => ctx.setHints(null);
|
|
24
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
25
|
+
}, [key]);
|
|
26
|
+
}
|
|
27
|
+
// The command a form or action equals, shown bottom right: "= vsf agents cancel ENG-12".
|
|
28
|
+
export function useEquivalent(args) {
|
|
29
|
+
const ctx = useScreen();
|
|
30
|
+
const key = JSON.stringify(args);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
ctx.setCommand(args);
|
|
33
|
+
return () => ctx.setCommand(null);
|
|
34
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
35
|
+
}, [key]);
|
|
36
|
+
}
|
|
37
|
+
// Turns global keys off while a field is focused.
|
|
38
|
+
export function useCapture(active) {
|
|
39
|
+
const ctx = useScreen();
|
|
40
|
+
useEffect(() => (active ? ctx.capture() : undefined), [active]);
|
|
41
|
+
}
|
|
42
|
+
export function useBackGuard(fn) {
|
|
43
|
+
const ctx = useScreen();
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
ctx.setBackGuard(fn);
|
|
46
|
+
return () => ctx.setBackGuard(null);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Check, Tool, ToolContext } from '../tool.ts';
|
|
3
|
+
export declare const POLL_MS = 2000;
|
|
4
|
+
export declare const CHECKS_MS = 30000;
|
|
5
|
+
export interface AgentsCurrent {
|
|
6
|
+
pid: number;
|
|
7
|
+
card?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
workflow?: string;
|
|
10
|
+
step?: string;
|
|
11
|
+
round?: number;
|
|
12
|
+
startedAt?: string;
|
|
13
|
+
runDir?: string;
|
|
14
|
+
checking?: boolean;
|
|
15
|
+
elapsedMinutes?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface AgentsStatus {
|
|
18
|
+
engine?: string;
|
|
19
|
+
runner: 'running' | 'paused' | 'stopped';
|
|
20
|
+
lastTick: {
|
|
21
|
+
at: string;
|
|
22
|
+
line: string;
|
|
23
|
+
} | null;
|
|
24
|
+
current: AgentsCurrent | null;
|
|
25
|
+
parked: {
|
|
26
|
+
card: string;
|
|
27
|
+
reason?: string;
|
|
28
|
+
}[];
|
|
29
|
+
interrupted: string[];
|
|
30
|
+
workflows: number;
|
|
31
|
+
projects: number;
|
|
32
|
+
}
|
|
33
|
+
export interface StepRecord {
|
|
34
|
+
step: string;
|
|
35
|
+
label: string;
|
|
36
|
+
startedAt: string;
|
|
37
|
+
endedAt?: string;
|
|
38
|
+
result?: string;
|
|
39
|
+
summary?: string;
|
|
40
|
+
log?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface RunRecord {
|
|
43
|
+
card: string;
|
|
44
|
+
title: string;
|
|
45
|
+
url?: string;
|
|
46
|
+
workflow: string;
|
|
47
|
+
trigger?: string;
|
|
48
|
+
steps: StepRecord[];
|
|
49
|
+
outcome: 'running' | 'verified' | 'changes' | 'parked' | 'cancelled';
|
|
50
|
+
reason?: string;
|
|
51
|
+
prUrl?: string;
|
|
52
|
+
startedAt: string;
|
|
53
|
+
endedAt?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface RunItem {
|
|
56
|
+
card: string;
|
|
57
|
+
stamp: string;
|
|
58
|
+
dir: string;
|
|
59
|
+
logs: string[];
|
|
60
|
+
record?: RunRecord;
|
|
61
|
+
}
|
|
62
|
+
export interface WorkflowItem {
|
|
63
|
+
name: string;
|
|
64
|
+
triggers: {
|
|
65
|
+
source: string;
|
|
66
|
+
team: string;
|
|
67
|
+
label: string;
|
|
68
|
+
}[];
|
|
69
|
+
steps: string[];
|
|
70
|
+
maxRounds: number;
|
|
71
|
+
project: string;
|
|
72
|
+
}
|
|
73
|
+
export interface ProjectItem {
|
|
74
|
+
name: string;
|
|
75
|
+
path: string;
|
|
76
|
+
base?: string;
|
|
77
|
+
usedBy?: string[];
|
|
78
|
+
}
|
|
79
|
+
export interface HomeItem {
|
|
80
|
+
[key: string]: unknown;
|
|
81
|
+
}
|
|
82
|
+
export interface AwakeStatus {
|
|
83
|
+
on: boolean;
|
|
84
|
+
sleepDisabledByOther: boolean;
|
|
85
|
+
since: string | null;
|
|
86
|
+
until: string | null;
|
|
87
|
+
minutesLeft: number | null;
|
|
88
|
+
batteryFloor: number;
|
|
89
|
+
floorApplies: boolean;
|
|
90
|
+
power: {
|
|
91
|
+
onBattery: boolean;
|
|
92
|
+
percent: number | null;
|
|
93
|
+
};
|
|
94
|
+
watchdog: boolean;
|
|
95
|
+
lastReason: string | null;
|
|
96
|
+
}
|
|
97
|
+
export interface MemoryStatus {
|
|
98
|
+
vault: string;
|
|
99
|
+
isVault: boolean;
|
|
100
|
+
notes: number;
|
|
101
|
+
indexed: number;
|
|
102
|
+
unindexed: {
|
|
103
|
+
count: number;
|
|
104
|
+
names: string[];
|
|
105
|
+
};
|
|
106
|
+
lastUpdated: string | null;
|
|
107
|
+
homes: {
|
|
108
|
+
kind: 'claude' | 'codex';
|
|
109
|
+
dir: string;
|
|
110
|
+
recall: string;
|
|
111
|
+
skill: string;
|
|
112
|
+
}[];
|
|
113
|
+
}
|
|
114
|
+
export interface StepsCatalog {
|
|
115
|
+
steps: {
|
|
116
|
+
id: string;
|
|
117
|
+
name: string;
|
|
118
|
+
label: string;
|
|
119
|
+
hint: string;
|
|
120
|
+
output: string;
|
|
121
|
+
}[];
|
|
122
|
+
templates: Record<string, {
|
|
123
|
+
steps: string[];
|
|
124
|
+
maxRounds: number;
|
|
125
|
+
}>;
|
|
126
|
+
}
|
|
127
|
+
export interface UpdateInfo {
|
|
128
|
+
current: string;
|
|
129
|
+
latest: string | null;
|
|
130
|
+
checked: boolean;
|
|
131
|
+
}
|
|
132
|
+
export interface DataState {
|
|
133
|
+
version: string;
|
|
134
|
+
tools: string[];
|
|
135
|
+
agents: AgentsStatus | null;
|
|
136
|
+
awake: AwakeStatus | null;
|
|
137
|
+
memory: MemoryStatus | null;
|
|
138
|
+
runs: RunItem[];
|
|
139
|
+
workflows: WorkflowItem[];
|
|
140
|
+
projects: ProjectItem[];
|
|
141
|
+
homes: HomeItem[];
|
|
142
|
+
agentsConfig: Record<string, unknown> | null;
|
|
143
|
+
steps: StepsCatalog | null;
|
|
144
|
+
checks: Check[] | null;
|
|
145
|
+
checksAt: number | null;
|
|
146
|
+
update: UpdateInfo;
|
|
147
|
+
errors: Record<string, string>;
|
|
148
|
+
loaded: boolean;
|
|
149
|
+
at: number;
|
|
150
|
+
}
|
|
151
|
+
export interface Data {
|
|
152
|
+
state: DataState;
|
|
153
|
+
refresh(): void;
|
|
154
|
+
refreshChecks(): void;
|
|
155
|
+
readConfig(tool: string): Record<string, unknown> | null;
|
|
156
|
+
help(tool: string): Promise<string>;
|
|
157
|
+
}
|
|
158
|
+
export declare function emptyState(tools?: string[]): DataState;
|
|
159
|
+
export declare function captureJson(tool: Pick<Tool, 'commands'>, command: string, args: string[], base: ToolContext): Promise<unknown>;
|
|
160
|
+
export declare function captureText(tool: Pick<Tool, 'commands'>, command: string, args: string[], base: ToolContext): Promise<string>;
|
|
161
|
+
export declare const messageOf: (err: unknown) => string;
|
|
162
|
+
export declare function runsThisWeek(runs: RunItem[], nowMs: number): number;
|
|
163
|
+
export declare function updateNotice(update: UpdateInfo): string | null;
|
|
164
|
+
export declare class Poller {
|
|
165
|
+
state: DataState;
|
|
166
|
+
private listeners;
|
|
167
|
+
private tools;
|
|
168
|
+
private timer;
|
|
169
|
+
private busy;
|
|
170
|
+
private again;
|
|
171
|
+
private checking;
|
|
172
|
+
private stopped;
|
|
173
|
+
private lastChecks;
|
|
174
|
+
private base;
|
|
175
|
+
subscribe: (fn: () => void) => (() => void);
|
|
176
|
+
private set;
|
|
177
|
+
start(): void;
|
|
178
|
+
stop(): void;
|
|
179
|
+
refresh: () => void;
|
|
180
|
+
refreshChecks: () => void;
|
|
181
|
+
readToolConfig: (name: string) => Record<string, unknown> | null;
|
|
182
|
+
helpText: (name: string) => Promise<string>;
|
|
183
|
+
private cancelUpdate;
|
|
184
|
+
private checkUpdate;
|
|
185
|
+
private load;
|
|
186
|
+
private poll;
|
|
187
|
+
private keepCheckError;
|
|
188
|
+
private pollAgents;
|
|
189
|
+
private runChecks;
|
|
190
|
+
}
|
|
191
|
+
export declare const DataContext: React.Context<Data | null>;
|
|
192
|
+
export declare function useData(): Data;
|
|
193
|
+
export declare function DataProvider(props: {
|
|
194
|
+
poller?: Poller;
|
|
195
|
+
children?: React.ReactNode;
|
|
196
|
+
}): React.ReactElement;
|
package/dist/tui/data.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
// The dashboard's store. While the dashboard is open it polls every 2 seconds:
|
|
10
|
+
// each added tool's `status --json` (and for agents also runs, workflows,
|
|
11
|
+
// projects and homes), runs the doctor-style checks every 30 seconds, and asks
|
|
12
|
+
// npm once for a newer vsf. Everything is read in-process with a capturing
|
|
13
|
+
// ctx.log, so nothing ever prints to the real stdout while Ink is mounted.
|
|
14
|
+
// A failing source becomes a string in `errors[source]`; it never throws.
|
|
15
|
+
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
16
|
+
import { execFile } from 'node:child_process';
|
|
17
|
+
import { baseChecks, run } from "../checks.js";
|
|
18
|
+
import { configDir, context, readConfig } from "../config.js";
|
|
19
|
+
import { locate } from "../loader.js";
|
|
20
|
+
import { findTool } from "../registry.js";
|
|
21
|
+
import { CLI_PKG, cliVersion, isNewer } from "../version.js";
|
|
22
|
+
const h = React.createElement;
|
|
23
|
+
export const POLL_MS = 2000;
|
|
24
|
+
export const CHECKS_MS = 30_000;
|
|
25
|
+
export function emptyState(tools = []) {
|
|
26
|
+
return {
|
|
27
|
+
version: cliVersion(),
|
|
28
|
+
tools,
|
|
29
|
+
agents: null,
|
|
30
|
+
awake: null,
|
|
31
|
+
memory: null,
|
|
32
|
+
runs: [],
|
|
33
|
+
workflows: [],
|
|
34
|
+
projects: [],
|
|
35
|
+
homes: [],
|
|
36
|
+
agentsConfig: null,
|
|
37
|
+
steps: null,
|
|
38
|
+
checks: null,
|
|
39
|
+
checksAt: null,
|
|
40
|
+
update: { current: cliVersion(), latest: null, checked: false },
|
|
41
|
+
errors: {},
|
|
42
|
+
loaded: false,
|
|
43
|
+
at: 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// ---- pure helpers --------------------------------------------------------
|
|
47
|
+
// Runs a tool command with a capturing log and parses its JSON output.
|
|
48
|
+
export async function captureJson(tool, command, args, base) {
|
|
49
|
+
const fn = tool.commands?.[command];
|
|
50
|
+
if (!fn)
|
|
51
|
+
throw new Error(`no ${command} command`);
|
|
52
|
+
let out = '';
|
|
53
|
+
const code = await fn(args, { ...base, log: (line) => (out += line + '\n') });
|
|
54
|
+
if (code !== 0)
|
|
55
|
+
throw new Error(out.trim().split('\n')[0] || `${command} exited ${code}`);
|
|
56
|
+
return JSON.parse(out);
|
|
57
|
+
}
|
|
58
|
+
// Captures a tool command's text output (for the palette's command list).
|
|
59
|
+
export async function captureText(tool, command, args, base) {
|
|
60
|
+
const fn = tool.commands?.[command];
|
|
61
|
+
if (!fn)
|
|
62
|
+
return '';
|
|
63
|
+
let out = '';
|
|
64
|
+
await fn(args, { ...base, log: (line) => (out += line + '\n') });
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
export const messageOf = (err) => (err instanceof Error ? err.message : String(err)).split('\n')[0].slice(0, 200);
|
|
68
|
+
// Runs of the last 7 days.
|
|
69
|
+
export function runsThisWeek(runs, nowMs) {
|
|
70
|
+
const since = nowMs - 7 * 24 * 3600 * 1000;
|
|
71
|
+
return runs.filter((r) => {
|
|
72
|
+
const t = Date.parse(r.record?.startedAt ?? '');
|
|
73
|
+
return !Number.isNaN(t) && t >= since;
|
|
74
|
+
}).length;
|
|
75
|
+
}
|
|
76
|
+
// The newer version to announce, or null.
|
|
77
|
+
export function updateNotice(update) {
|
|
78
|
+
return update.latest && isNewer(update.latest, update.current) ? update.latest : null;
|
|
79
|
+
}
|
|
80
|
+
// ---- the poller ----------------------------------------------------------
|
|
81
|
+
// Asks npm for the latest version. Resolves null on any failure or timeout.
|
|
82
|
+
function latestAsync(pkg, timeout) {
|
|
83
|
+
let child;
|
|
84
|
+
const result = new Promise((resolve) => {
|
|
85
|
+
child = execFile('npm', ['view', '--prefer-online', pkg, 'version'], { timeout, encoding: 'utf8' }, (err, stdout) => resolve(err ? null : String(stdout).trim() || null));
|
|
86
|
+
});
|
|
87
|
+
return { result, cancel: () => child?.kill() };
|
|
88
|
+
}
|
|
89
|
+
export class Poller {
|
|
90
|
+
state = emptyState();
|
|
91
|
+
listeners = new Set();
|
|
92
|
+
tools = new Map();
|
|
93
|
+
timer = null;
|
|
94
|
+
busy = false;
|
|
95
|
+
again = false;
|
|
96
|
+
checking = false;
|
|
97
|
+
stopped = false;
|
|
98
|
+
lastChecks = 0;
|
|
99
|
+
base = context();
|
|
100
|
+
subscribe = (fn) => {
|
|
101
|
+
this.listeners.add(fn);
|
|
102
|
+
return () => this.listeners.delete(fn);
|
|
103
|
+
};
|
|
104
|
+
set(patch) {
|
|
105
|
+
this.state = { ...this.state, ...patch };
|
|
106
|
+
for (const fn of this.listeners)
|
|
107
|
+
fn();
|
|
108
|
+
}
|
|
109
|
+
start() {
|
|
110
|
+
void this.poll();
|
|
111
|
+
void this.checkUpdate();
|
|
112
|
+
this.timer = setInterval(() => void this.poll(), POLL_MS);
|
|
113
|
+
}
|
|
114
|
+
stop() {
|
|
115
|
+
this.stopped = true;
|
|
116
|
+
this.cancelUpdate?.();
|
|
117
|
+
if (this.timer)
|
|
118
|
+
clearInterval(this.timer);
|
|
119
|
+
this.listeners.clear();
|
|
120
|
+
}
|
|
121
|
+
refresh = () => {
|
|
122
|
+
void this.poll();
|
|
123
|
+
};
|
|
124
|
+
refreshChecks = () => {
|
|
125
|
+
this.lastChecks = 0;
|
|
126
|
+
void this.poll();
|
|
127
|
+
};
|
|
128
|
+
readToolConfig = (name) => {
|
|
129
|
+
const tool = this.tools.get(name);
|
|
130
|
+
try {
|
|
131
|
+
return tool?.config ? tool.config.read(this.base.configDir) : null;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
helpText = async (name) => {
|
|
138
|
+
try {
|
|
139
|
+
return await captureText(await this.load(name), 'help', [], this.base);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
cancelUpdate = null;
|
|
146
|
+
async checkUpdate() {
|
|
147
|
+
const ask = latestAsync(CLI_PKG, 3000);
|
|
148
|
+
this.cancelUpdate = ask.cancel;
|
|
149
|
+
const latest = await ask.result;
|
|
150
|
+
if (!this.stopped)
|
|
151
|
+
this.set({ update: { current: cliVersion(), latest, checked: true } });
|
|
152
|
+
}
|
|
153
|
+
// Tools are loaded from disk only: loadTool would run npm to fetch a
|
|
154
|
+
// missing one, and that must never block the screen.
|
|
155
|
+
async load(name) {
|
|
156
|
+
const cached = this.tools.get(name);
|
|
157
|
+
if (cached)
|
|
158
|
+
return cached;
|
|
159
|
+
const entry = findTool(name);
|
|
160
|
+
if (!entry)
|
|
161
|
+
throw new Error(`unknown tool ${name}`);
|
|
162
|
+
const path = locate(entry, configDir());
|
|
163
|
+
if (!path)
|
|
164
|
+
throw new Error('not downloaded. Run vsf update');
|
|
165
|
+
const { pathToFileURL } = await import('node:url');
|
|
166
|
+
const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(path).href)));
|
|
167
|
+
this.tools.set(name, mod.default);
|
|
168
|
+
return mod.default;
|
|
169
|
+
}
|
|
170
|
+
async poll() {
|
|
171
|
+
if (this.busy) {
|
|
172
|
+
this.again = true;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.busy = true;
|
|
176
|
+
try {
|
|
177
|
+
let names = [];
|
|
178
|
+
const errors = {};
|
|
179
|
+
try {
|
|
180
|
+
names = readConfig().tools.filter((n) => !!findTool(n));
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
errors.config = messageOf(err);
|
|
184
|
+
}
|
|
185
|
+
const patch = { tools: names };
|
|
186
|
+
await Promise.all(names.map(async (name) => {
|
|
187
|
+
try {
|
|
188
|
+
const tool = await this.load(name);
|
|
189
|
+
const status = (await captureJson(tool, 'status', ['--json'], this.base));
|
|
190
|
+
if (name === 'agents')
|
|
191
|
+
patch.agents = status;
|
|
192
|
+
if (name === 'awake')
|
|
193
|
+
patch.awake = status;
|
|
194
|
+
if (name === 'memory')
|
|
195
|
+
patch.memory = status;
|
|
196
|
+
if (name === 'agents')
|
|
197
|
+
await this.pollAgents(tool, patch, errors);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
errors[name] = messageOf(err);
|
|
201
|
+
}
|
|
202
|
+
}));
|
|
203
|
+
if (!names.includes('agents'))
|
|
204
|
+
Object.assign(patch, { agents: null, runs: [], workflows: [], projects: [], homes: [], agentsConfig: null });
|
|
205
|
+
if (!names.includes('awake'))
|
|
206
|
+
patch.awake = null;
|
|
207
|
+
if (!names.includes('memory'))
|
|
208
|
+
patch.memory = null;
|
|
209
|
+
this.set({ ...patch, errors: { ...errors, ...this.keepCheckError() }, loaded: true, at: Date.now() });
|
|
210
|
+
if (Date.now() - this.lastChecks >= CHECKS_MS)
|
|
211
|
+
void this.runChecks(names);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
this.busy = false;
|
|
215
|
+
if (this.again && !this.stopped) {
|
|
216
|
+
this.again = false;
|
|
217
|
+
void this.poll();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
keepCheckError() {
|
|
222
|
+
const e = this.state.errors.checks;
|
|
223
|
+
return e ? { checks: e } : {};
|
|
224
|
+
}
|
|
225
|
+
async pollAgents(tool, patch, errors) {
|
|
226
|
+
const get = async (key, command, args, into) => {
|
|
227
|
+
try {
|
|
228
|
+
into((await captureJson(tool, command, args, this.base)));
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
errors[`agents.${key}`] = messageOf(err);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
await Promise.all([
|
|
235
|
+
get('runs', 'runs', ['--json', '--limit', '60'], (v) => (patch.runs = v)),
|
|
236
|
+
get('workflows', 'workflows', ['--json'], (v) => (patch.workflows = v)),
|
|
237
|
+
get('projects', 'projects', ['--json'], (v) => (patch.projects = v)),
|
|
238
|
+
get('homes', 'homes', ['--json'], (v) => (patch.homes = v)),
|
|
239
|
+
// The step catalog never changes while the dashboard is open.
|
|
240
|
+
this.state.steps ? Promise.resolve() : get('steps', 'steps', [], (v) => (patch.steps = v)),
|
|
241
|
+
]);
|
|
242
|
+
patch.agentsConfig = this.readToolConfig('agents');
|
|
243
|
+
}
|
|
244
|
+
// Doctor-style checks: base checks plus each added tool's own, in-process.
|
|
245
|
+
async runChecks(names) {
|
|
246
|
+
if (this.checking || this.stopped)
|
|
247
|
+
return;
|
|
248
|
+
this.checking = true;
|
|
249
|
+
this.lastChecks = Date.now();
|
|
250
|
+
try {
|
|
251
|
+
if (process.env.VSF_TUI_SKIP_CHECKS === '1') {
|
|
252
|
+
this.set({ checks: [], checksAt: Date.now() });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const checks = baseChecks(process.platform, process.version, run);
|
|
256
|
+
for (const name of names) {
|
|
257
|
+
try {
|
|
258
|
+
const tool = await this.load(name);
|
|
259
|
+
checks.push(...(await tool.checks({ ...this.base, log: () => { } })));
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
checks.push({ name, status: 'fail', detail: messageOf(err) });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!this.stopped) {
|
|
266
|
+
const { checks: _gone, ...errors } = this.state.errors;
|
|
267
|
+
this.set({ checks, checksAt: Date.now(), errors });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
this.set({ errors: { ...this.state.errors, checks: messageOf(err) } });
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
this.checking = false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// ---- React glue ----------------------------------------------------------
|
|
279
|
+
export const DataContext = createContext(null);
|
|
280
|
+
export function useData() {
|
|
281
|
+
const data = useContext(DataContext);
|
|
282
|
+
if (!data)
|
|
283
|
+
throw new Error('useData needs a DataProvider');
|
|
284
|
+
return data;
|
|
285
|
+
}
|
|
286
|
+
export function DataProvider(props) {
|
|
287
|
+
const ref = useRef(null);
|
|
288
|
+
if (!ref.current)
|
|
289
|
+
ref.current = props.poller ?? new Poller();
|
|
290
|
+
const poller = ref.current;
|
|
291
|
+
const [state, setState] = useState(poller.state);
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
const off = poller.subscribe(() => setState(poller.state));
|
|
294
|
+
poller.start();
|
|
295
|
+
return () => {
|
|
296
|
+
off();
|
|
297
|
+
poller.stop();
|
|
298
|
+
};
|
|
299
|
+
}, [poller]);
|
|
300
|
+
const value = { state, refresh: poller.refresh, refreshChecks: poller.refreshChecks, readConfig: poller.readToolConfig, help: poller.helpText };
|
|
301
|
+
return h(DataContext.Provider, { value }, props.children);
|
|
302
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const clock: (d: Date) => string;
|
|
2
|
+
export declare const stamp: (d: Date) => string;
|
|
3
|
+
export declare function stampDate(folder: string): Date;
|
|
4
|
+
export declare function folderStamp(folder: string): string;
|
|
5
|
+
export declare function clockOrStamp(d: Date, now?: Date): string;
|
|
6
|
+
export declare function duration(ms: number): string;
|
|
7
|
+
export declare function minutesText(min: number): string;
|
|
8
|
+
export declare const plural: (n: number, word: string) => string;
|
|
9
|
+
export declare function truncate(text: string, max: number): string;
|
|
10
|
+
export declare function tilde(path: string, home?: string | undefined): string;
|
|
11
|
+
export declare function secondsToNextCheck(lastAt: string | undefined, nowMs: number, everySec?: number): number | null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Small pure formatters shared by the dashboard screens.
|
|
2
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
3
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
4
|
+
// "14:03" in local time.
|
|
5
|
+
export const clock = (d) => `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
6
|
+
// "Oct 20 13:51" in local time.
|
|
7
|
+
export const stamp = (d) => `${MONTHS[d.getMonth()]} ${d.getDate()} ${clock(d)}`;
|
|
8
|
+
// A run folder name like "2026-10-21T03-31-12-345Z" (ISO with ':' and '.' turned
|
|
9
|
+
// into '-') as a Date. Anything else is handed to Date as is.
|
|
10
|
+
export function stampDate(folder) {
|
|
11
|
+
const m = /^(\d{4}-\d\d-\d\d)T(\d\d)-(\d\d)-(\d\d)-(\d+)Z$/.exec(folder);
|
|
12
|
+
return new Date(m ? `${m[1]}T${m[2]}:${m[3]}:${m[4]}.${m[5]}Z` : folder);
|
|
13
|
+
}
|
|
14
|
+
// A run folder name as local "Oct 20 21:31", or "?" when it is not a date.
|
|
15
|
+
export function folderStamp(folder) {
|
|
16
|
+
const d = stampDate(folder);
|
|
17
|
+
return Number.isNaN(d.getTime()) ? '?' : stamp(d);
|
|
18
|
+
}
|
|
19
|
+
// Clock time for today, date and time for any other day.
|
|
20
|
+
export function clockOrStamp(d, now = new Date()) {
|
|
21
|
+
return d.toDateString() === now.toDateString() ? clock(d) : stamp(d);
|
|
22
|
+
}
|
|
23
|
+
// "<1m", "12m", "1h 4m".
|
|
24
|
+
export function duration(ms) {
|
|
25
|
+
const min = Math.round(ms / 60_000);
|
|
26
|
+
if (min < 1)
|
|
27
|
+
return '<1m';
|
|
28
|
+
return min < 60 ? `${min}m` : `${Math.floor(min / 60)}h${min % 60 ? ` ${min % 60}m` : ''}`;
|
|
29
|
+
}
|
|
30
|
+
// "8h 50m" from whole minutes.
|
|
31
|
+
export function minutesText(min) {
|
|
32
|
+
if (min < 60)
|
|
33
|
+
return `${Math.max(0, Math.round(min))}m`;
|
|
34
|
+
return `${Math.floor(min / 60)}h ${Math.round(min % 60)}m`;
|
|
35
|
+
}
|
|
36
|
+
export const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
37
|
+
// Cuts to max characters, ending in an ellipsis when it had to cut.
|
|
38
|
+
export function truncate(text, max) {
|
|
39
|
+
const t = text.replace(/\s+/g, ' ').trim();
|
|
40
|
+
return t.length > max ? t.slice(0, Math.max(0, max - 1)) + '…' : t;
|
|
41
|
+
}
|
|
42
|
+
// Replaces the home folder with ~.
|
|
43
|
+
export function tilde(path, home = process.env.HOME) {
|
|
44
|
+
return home && path.startsWith(home) ? '~' + path.slice(home.length) : path;
|
|
45
|
+
}
|
|
46
|
+
// Seconds until the next runner check, given the last one. null when unknown.
|
|
47
|
+
export function secondsToNextCheck(lastAt, nowMs, everySec = 60) {
|
|
48
|
+
if (!lastAt)
|
|
49
|
+
return null;
|
|
50
|
+
const t = Date.parse(lastAt);
|
|
51
|
+
if (Number.isNaN(t))
|
|
52
|
+
return null;
|
|
53
|
+
return Math.max(0, Math.round((t + everySec * 1000 - nowMs) / 1000));
|
|
54
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type AgentsStatus, type AwakeStatus, type DataState, type MemoryStatus, type RunItem } from './data.ts';
|
|
3
|
+
import { type Hint } from './context.ts';
|
|
4
|
+
import { type Line, type StepProgress } from './widgets.ts';
|
|
5
|
+
import type { Check } from '../tool.ts';
|
|
6
|
+
import type { Screen } from './screens.ts';
|
|
7
|
+
export type SectionId = 'agents' | 'awake' | 'memory' | 'checks';
|
|
8
|
+
export declare function sectionsFor(tools: string[]): SectionId[];
|
|
9
|
+
export declare function nextSection(sections: SectionId[], current: SectionId, dir: 1 | -1): SectionId;
|
|
10
|
+
export declare const clampFocus: (sections: SectionId[], current: SectionId) => SectionId;
|
|
11
|
+
export interface LiveCard {
|
|
12
|
+
card: string;
|
|
13
|
+
title: string;
|
|
14
|
+
workflow: string;
|
|
15
|
+
step: string;
|
|
16
|
+
minutes: number;
|
|
17
|
+
round: number;
|
|
18
|
+
maxRounds: number | null;
|
|
19
|
+
steps: StepProgress[];
|
|
20
|
+
}
|
|
21
|
+
export declare function liveCard(state: Pick<DataState, 'agents' | 'runs' | 'workflows' | 'steps' | 'agentsConfig'>, nowMs: number): LiveCard | null;
|
|
22
|
+
export declare function runnerLines(agents: AgentsStatus, nowMs: number): {
|
|
23
|
+
left: Line;
|
|
24
|
+
right: Line;
|
|
25
|
+
};
|
|
26
|
+
export interface ParkedRow {
|
|
27
|
+
card: string;
|
|
28
|
+
kind: 'parked' | 'interrupted';
|
|
29
|
+
text: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function parkedRows(agents: AgentsStatus): ParkedRow[];
|
|
32
|
+
export declare function countsLine(agents: AgentsStatus, runs: RunItem[], nowMs: number): string;
|
|
33
|
+
export declare function awakeLines(a: AwakeStatus): Line[];
|
|
34
|
+
export declare function memoryLines(m: MemoryStatus, now?: Date): Line[];
|
|
35
|
+
export interface ChecksView {
|
|
36
|
+
ok: number;
|
|
37
|
+
warn: number;
|
|
38
|
+
fail: number;
|
|
39
|
+
problems: Check[];
|
|
40
|
+
}
|
|
41
|
+
export declare function checksView(checks: Check[]): ChecksView;
|
|
42
|
+
export declare function checksLines(checks: Check[]): {
|
|
43
|
+
left: Line;
|
|
44
|
+
right: Line;
|
|
45
|
+
}[];
|
|
46
|
+
export declare function homeHints(opts: {
|
|
47
|
+
runner?: AgentsStatus['runner'];
|
|
48
|
+
hasAwake: boolean;
|
|
49
|
+
}): Hint[];
|
|
50
|
+
export declare const homeScreen: Screen;
|
|
51
|
+
export declare function Home(): React.ReactElement;
|