@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.
Files changed (89) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +136 -2
  3. package/dist/checks.d.ts +9 -0
  4. package/dist/checks.js +51 -0
  5. package/dist/commands/args.d.ts +13 -0
  6. package/dist/commands/args.js +22 -0
  7. package/dist/commands/config.d.ts +1 -0
  8. package/dist/commands/config.js +181 -0
  9. package/dist/commands/doctor.d.ts +3 -0
  10. package/dist/commands/doctor.js +52 -0
  11. package/dist/commands/help.d.ts +3 -0
  12. package/dist/commands/help.js +59 -0
  13. package/dist/commands/setup.d.ts +2 -0
  14. package/dist/commands/setup.js +135 -0
  15. package/dist/commands/status.d.ts +2 -0
  16. package/dist/commands/status.js +60 -0
  17. package/dist/commands/tools.d.ts +9 -0
  18. package/dist/commands/tools.js +111 -0
  19. package/dist/commands/update.d.ts +2 -0
  20. package/dist/commands/update.js +79 -0
  21. package/dist/config.d.ts +9 -0
  22. package/dist/config.js +29 -0
  23. package/dist/dotted.d.ts +9 -0
  24. package/dist/dotted.js +86 -0
  25. package/dist/loader.d.ts +6 -0
  26. package/dist/loader.js +51 -0
  27. package/dist/main.d.ts +2 -0
  28. package/dist/main.js +93 -0
  29. package/dist/registry.d.ts +8 -0
  30. package/dist/registry.js +26 -0
  31. package/dist/tool.d.ts +51 -0
  32. package/dist/tool.js +4 -0
  33. package/dist/tui/actions.d.ts +17 -0
  34. package/dist/tui/actions.js +99 -0
  35. package/dist/tui/app.d.ts +6 -0
  36. package/dist/tui/app.js +246 -0
  37. package/dist/tui/context.d.ts +62 -0
  38. package/dist/tui/context.js +48 -0
  39. package/dist/tui/data.d.ts +196 -0
  40. package/dist/tui/data.js +302 -0
  41. package/dist/tui/format.d.ts +11 -0
  42. package/dist/tui/format.js +54 -0
  43. package/dist/tui/home.d.ts +51 -0
  44. package/dist/tui/home.js +336 -0
  45. package/dist/tui/keys.d.ts +2 -0
  46. package/dist/tui/keys.js +107 -0
  47. package/dist/tui/nav.d.ts +24 -0
  48. package/dist/tui/nav.js +32 -0
  49. package/dist/tui/palette.d.ts +32 -0
  50. package/dist/tui/palette.js +269 -0
  51. package/dist/tui/run.d.ts +1 -0
  52. package/dist/tui/run.js +20 -0
  53. package/dist/tui/screens/agents.d.ts +6 -0
  54. package/dist/tui/screens/agents.js +44 -0
  55. package/dist/tui/screens/awake.d.ts +23 -0
  56. package/dist/tui/screens/awake.js +209 -0
  57. package/dist/tui/screens/checks.d.ts +11 -0
  58. package/dist/tui/screens/checks.js +106 -0
  59. package/dist/tui/screens/homes.d.ts +14 -0
  60. package/dist/tui/screens/homes.js +99 -0
  61. package/dist/tui/screens/list-window.d.ts +6 -0
  62. package/dist/tui/screens/list-window.js +18 -0
  63. package/dist/tui/screens/log.d.ts +26 -0
  64. package/dist/tui/screens/log.js +266 -0
  65. package/dist/tui/screens/memory.d.ts +24 -0
  66. package/dist/tui/screens/memory.js +174 -0
  67. package/dist/tui/screens/project-form.d.ts +14 -0
  68. package/dist/tui/screens/project-form.js +137 -0
  69. package/dist/tui/screens/projects.d.ts +17 -0
  70. package/dist/tui/screens/projects.js +109 -0
  71. package/dist/tui/screens/runs.d.ts +43 -0
  72. package/dist/tui/screens/runs.js +420 -0
  73. package/dist/tui/screens/settings.d.ts +17 -0
  74. package/dist/tui/screens/settings.js +153 -0
  75. package/dist/tui/screens/workflow-form.d.ts +58 -0
  76. package/dist/tui/screens/workflow-form.js +375 -0
  77. package/dist/tui/screens/workflows.d.ts +41 -0
  78. package/dist/tui/screens/workflows.js +174 -0
  79. package/dist/tui/screens.d.ts +30 -0
  80. package/dist/tui/screens.js +172 -0
  81. package/dist/tui/theme.d.ts +14 -0
  82. package/dist/tui/theme.js +18 -0
  83. package/dist/tui/widgets.d.ts +122 -0
  84. package/dist/tui/widgets.js +264 -0
  85. package/dist/ui.d.ts +5 -0
  86. package/dist/ui.js +82 -0
  87. package/dist/version.d.ts +3 -0
  88. package/dist/version.js +17 -0
  89. package/package.json +59 -4
@@ -0,0 +1,174 @@
1
+ // Memory: the vault, what is not indexed, and recall per config folder. Try
2
+ // and check run the real commands (vsf memory try / check --json); on and off
3
+ // run vsf memory on|off DIR.
4
+ import React, { useState } from 'react';
5
+ import { Box, Text } from 'ink';
6
+ import { runAction } from "../actions.js";
7
+ import { useData } from "../data.js";
8
+ import { useBackGuard, useEquivalent, useHints, useKeys, useScreen } from "../context.js";
9
+ import { clockOrStamp, plural, tilde, truncate } from "../format.js";
10
+ import { theme } from "../theme.js";
11
+ import { Lines, Panel, seg, TextInput } from "../widgets.js";
12
+ import { scrollBy } from "./list-window.js";
13
+ const h = React.createElement;
14
+ export const kindLabel = (k) => (k === 'codex' ? 'Codex' : 'Claude Code');
15
+ export function recallText(recall) {
16
+ if (recall === 'on')
17
+ return { text: 'recall on', colour: theme.green };
18
+ if (recall === 'other')
19
+ return { text: 'another hook recalls', colour: theme.yellow };
20
+ return { text: 'recall off', colour: theme.dim };
21
+ }
22
+ // vsf memory on|off DIR for the chosen home.
23
+ export const toggleArgs = (mode, home) => ['memory', mode, home.dir];
24
+ export const tryArgs = (prompt) => ['memory', 'try', ...(prompt.startsWith('-') ? ['--'] : []), prompt];
25
+ export function parseProblems(output) {
26
+ try {
27
+ const v = JSON.parse(output);
28
+ return { unindexed: v.unindexed ?? [], brokenIndex: v.brokenIndex ?? [], brokenLinks: v.brokenLinks ?? [] };
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ // The problems grouped as the check command groups them.
35
+ export function problemLines(p) {
36
+ const lines = [];
37
+ const group = (title, items) => {
38
+ if (!items.length)
39
+ return;
40
+ lines.push([seg(`${title} (${items.length})`, theme.yellow)]);
41
+ for (const i of items)
42
+ lines.push([seg(` ${i}`)]);
43
+ };
44
+ group('Not in INDEX.md, never recalled', p.unindexed);
45
+ group('INDEX.md links to files that do not exist', p.brokenIndex);
46
+ group('[[links]] that match no note', p.brokenLinks.map((l) => `${l.note}: [[${l.link}]]`));
47
+ if (!lines.length)
48
+ lines.push([seg('Vault looks good. Every note is indexed and every link works.', theme.green)]);
49
+ return lines;
50
+ }
51
+ export function vaultLines(m, now = new Date()) {
52
+ const label = (t) => seg(t.padEnd(14), theme.dim);
53
+ const lines = [[label('Vault'), seg(tilde(m.vault))]];
54
+ if (!m.isVault) {
55
+ lines.push([seg('This folder has no INDEX.md, so it is not a vault yet. Run vsf setup memory.', theme.yellow)]);
56
+ return lines;
57
+ }
58
+ lines.push([label('Notes'), seg(`${m.notes}`), seg(` ${m.indexed} in INDEX.md`, theme.dim)]);
59
+ lines.push([label('Last updated'), seg(m.lastUpdated ? clockOrStamp(new Date(m.lastUpdated), now) : 'never')]);
60
+ if (m.unindexed.count) {
61
+ const names = m.unindexed.names.slice(0, 5).map((n) => truncate(n, 40));
62
+ lines.push([label('Not indexed'), seg(plural(m.unindexed.count, 'note'), theme.yellow), seg(' never recalled. c shows the details', theme.dim)]);
63
+ for (const n of names)
64
+ lines.push([seg(` ${n}`, theme.dim)]);
65
+ if (m.unindexed.count > names.length)
66
+ lines.push([seg(` +${m.unindexed.count - names.length} more`, theme.dim)]);
67
+ }
68
+ else
69
+ lines.push([label('Not indexed'), seg('none', theme.green)]);
70
+ return lines;
71
+ }
72
+ function Memory() {
73
+ const ctx = useScreen();
74
+ const { state } = useData();
75
+ const m = state.memory;
76
+ const [sel, setSel] = useState(0);
77
+ const [asking, setAsking] = useState(false);
78
+ const [prompt, setPrompt] = useState('');
79
+ const [result, setResult] = useState(null);
80
+ const [offset, setOffset] = useState(0);
81
+ const [busy, setBusy] = useState(false);
82
+ const homes = m?.homes ?? [];
83
+ const at = Math.min(sel, Math.max(0, homes.length - 1));
84
+ const home = homes[at];
85
+ useBackGuard(() => {
86
+ if (asking) {
87
+ setAsking(false);
88
+ return true;
89
+ }
90
+ if (result) {
91
+ setResult(null);
92
+ return true;
93
+ }
94
+ return false;
95
+ });
96
+ useHints(asking
97
+ ? [{ key: 'enter', label: 'try it' }, { key: 'esc', label: 'cancel' }]
98
+ : [{ key: 't', label: 'try a prompt' }, { key: 'c', label: 'check' }, { key: 'o', label: 'recall on' }, { key: 'f', label: 'recall off' }, { key: 'esc', label: 'back' }]);
99
+ useEquivalent(asking ? tryArgs(prompt || 'PROMPT') : ['memory', 'status']);
100
+ const runRead = async (args, title, shape) => {
101
+ if (busy)
102
+ return;
103
+ setBusy(true);
104
+ ctx.toast(`Running vsf ${args.join(' ')}`, 'info');
105
+ const r = await runAction(args, { timeoutMs: 60_000 });
106
+ setBusy(false);
107
+ setOffset(0);
108
+ setResult({ title, lines: shape(r.output, r.code) });
109
+ };
110
+ const submitTry = (value) => {
111
+ const p = value.trim();
112
+ if (!p)
113
+ return;
114
+ setAsking(false);
115
+ void runRead(tryArgs(p), `vsf memory try`, (out) => (out ? out.split('\n').map((l) => [seg(l)]) : [[seg('Nothing would be recalled.', theme.dim)]]));
116
+ };
117
+ useKeys((input, key) => {
118
+ if (asking) {
119
+ if (key.escape)
120
+ setAsking(false);
121
+ return;
122
+ }
123
+ if (key.upArrow)
124
+ setSel(Math.max(0, at - 1));
125
+ else if (key.downArrow)
126
+ setSel(Math.min(homes.length - 1, at + 1));
127
+ else if (key.pageUp)
128
+ setOffset((o) => scrollBy(o, -8, 10000));
129
+ else if (key.pageDown)
130
+ setOffset((o) => scrollBy(o, 8, 10000));
131
+ else if (input === 't') {
132
+ setPrompt('');
133
+ setAsking(true);
134
+ }
135
+ else if (input === 'c') {
136
+ void runRead(['memory', 'check', '--json'], 'vsf memory check', (out, code) => {
137
+ const p = parseProblems(out);
138
+ return p ? problemLines(p) : [[seg(out || `Check failed (exit code ${code})`, theme.red)]];
139
+ });
140
+ }
141
+ else if ((input === 'o' || input === 'f') && home) {
142
+ const mode = input === 'o' ? 'on' : 'off';
143
+ void ctx.actions.run({
144
+ args: toggleArgs(mode, home),
145
+ title: `Turn recall ${mode} in ${kindLabel(home.kind)}?`,
146
+ body: mode === 'on'
147
+ ? `Adds the recall hook to ${tilde(home.dir)}, so matching notes are added to your prompts.`
148
+ : `Removes the recall hook from ${tilde(home.dir)}. The skill stays.`,
149
+ confirmLabel: `Turn ${mode}`,
150
+ cancelLabel: 'Leave it',
151
+ });
152
+ }
153
+ }, { whileCapturing: true });
154
+ if (!m) {
155
+ return h(Box, { paddingX: 1 }, h(Text, { color: state.errors.memory ? theme.red : theme.dim }, state.errors.memory ?? (state.loaded ? 'memory is not added. Run vsf add memory.' : 'Loading...')));
156
+ }
157
+ const size = Math.max(4, ctx.rows - 20);
158
+ return h(Box, { flexDirection: 'column' }, h(Panel, { title: 'Memory' }, h(Lines, { lines: vaultLines(m) })), h(Panel, { title: 'Config folders', focused: !result && !asking }, homes.length === 0 ? h(Text, { color: theme.dim }, 'No config folders saved. Run vsf setup memory.') : null, ...homes.map((hm, i) => {
159
+ const r = recallText(hm.recall);
160
+ const on = i === at;
161
+ const bg = on ? theme.selected : undefined;
162
+ return h(Box, { key: hm.dir, flexDirection: 'row' }, h(Text, { backgroundColor: bg, color: theme.accent }, on ? '▶ ' : ' '), h(Text, { backgroundColor: bg, bold: on }, kindLabel(hm.kind).padEnd(13)), h(Text, { backgroundColor: bg, color: theme.dim }, tilde(hm.dir).padEnd(24)), h(Text, { backgroundColor: bg, color: r.colour }, r.text.padEnd(22)), h(Text, { backgroundColor: bg, color: theme.dim }, `skill ${hm.skill}`));
163
+ })), asking
164
+ ? h(Panel, { title: 'Try a prompt', focused: true }, h(Text, { color: theme.dim }, 'Type a prompt to see what recall would add to it. Real sessions are not touched.'), h(TextInput, { value: prompt, onChange: setPrompt, onSubmit: submitTry, placeholder: 'e.g. how do we deploy the relay?' }))
165
+ : null, result
166
+ ? h(Panel, { title: result.title, subtitle: 'esc closes', focused: true }, h(Lines, { lines: result.lines.slice(offset, offset + size) }), result.lines.length > offset + size ? h(Text, { color: theme.dim }, `+${result.lines.length - offset - size} more lines, page down`) : null)
167
+ : null);
168
+ }
169
+ export const memoryScreen = {
170
+ id: 'memory',
171
+ title: 'Memory',
172
+ component: () => h(Memory),
173
+ hints: () => [{ key: 't', label: 'try a prompt' }, { key: 'c', label: 'check' }, { key: 'o', label: 'recall on' }, { key: 'f', label: 'recall off' }, { key: 'esc', label: 'back' }],
174
+ };
@@ -0,0 +1,14 @@
1
+ import type { Screen } from '../screens.ts';
2
+ export type ProjectField = 'name' | 'path' | 'base';
3
+ export interface ProjectDraft {
4
+ name: string;
5
+ path: string;
6
+ base: string;
7
+ }
8
+ export declare function expandPath(p: string, home?: string): string;
9
+ export declare function repoState(path: string, exists?: (p: string) => boolean): 'ok' | 'empty' | 'not-repo';
10
+ export declare function projectProblems(d: ProjectDraft, isNew: boolean, existing: string[], exists?: (p: string) => boolean): string[];
11
+ export declare function projectSaveArgs(d: ProjectDraft, isNew: boolean): string[];
12
+ export declare const fieldsFor: (isNew: boolean) => ProjectField[];
13
+ export declare function moveFocus(fields: ProjectField[], current: ProjectField, delta: number): ProjectField;
14
+ export declare const projectFormScreen: Screen;
@@ -0,0 +1,137 @@
1
+ // The project form: Name, Path, Base branch. Saves with
2
+ // `vsf agents project add|edit`, run as a child process.
3
+ import React, { useState } from 'react';
4
+ import { existsSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { join, resolve } from 'node:path';
7
+ import { Box, Text } from 'ink';
8
+ import { useData } from "../data.js";
9
+ import { useBackGuard, useEquivalent, useHints, useKeys, useScreen } from "../context.js";
10
+ import { theme } from "../theme.js";
11
+ import { Panel, TextInput } from "../widgets.js";
12
+ const h = React.createElement;
13
+ export function expandPath(p, home = homedir()) {
14
+ const t = p.trim();
15
+ if (t === '~')
16
+ return home;
17
+ if (t.startsWith('~/'))
18
+ return join(home, t.slice(2));
19
+ return t ? resolve(t) : t;
20
+ }
21
+ // ok: a git repository. missing: nothing typed yet. not-repo: anything else.
22
+ export function repoState(path, exists = existsSync) {
23
+ if (!path.trim())
24
+ return 'empty';
25
+ return exists(join(expandPath(path), '.git')) ? 'ok' : 'not-repo';
26
+ }
27
+ export function projectProblems(d, isNew, existing, exists) {
28
+ const out = [];
29
+ if (isNew) {
30
+ if (!d.name.trim())
31
+ out.push('Give the project a name.');
32
+ else if (existing.includes(d.name.trim()))
33
+ out.push(`A project named ${d.name.trim()} already exists.`);
34
+ }
35
+ const r = repoState(d.path, exists);
36
+ if (r === 'empty')
37
+ out.push('Give the path to a git repository.');
38
+ else if (r === 'not-repo')
39
+ out.push('That path is not a git repository.');
40
+ return out;
41
+ }
42
+ export function projectSaveArgs(d, isNew) {
43
+ const name = d.name.trim();
44
+ const path = d.path.trim();
45
+ const base = d.base.trim();
46
+ if (isNew)
47
+ return ['agents', 'project', 'add', name, '--path', path, ...(base ? ['--base', base] : [])];
48
+ // An empty --base on edit clears it, so PRs go to the repo's default branch.
49
+ return ['agents', 'project', 'edit', name, '--path', path, '--base', base];
50
+ }
51
+ export const fieldsFor = (isNew) => (isNew ? ['name', 'path', 'base'] : ['path', 'base']);
52
+ export function moveFocus(fields, current, delta) {
53
+ const i = Math.max(0, fields.indexOf(current));
54
+ return fields[(i + delta + fields.length) % fields.length];
55
+ }
56
+ const KEY_HINTS = [
57
+ { key: 'tab', label: 'next field' },
58
+ { key: 'ctrl+s', label: 'save' },
59
+ { key: 'esc', label: 'discard' },
60
+ ];
61
+ function Row(props) {
62
+ return h(Box, { flexDirection: 'column' }, h(Box, null, h(Box, { width: 14, flexShrink: 0 }, h(Text, { bold: props.focused, color: props.focused ? theme.accent : theme.text }, props.label)), h(Box, { flexGrow: 1 }, props.children)), props.note ? h(Box, { paddingLeft: 14 }, props.note) : null);
63
+ }
64
+ function ProjectForm(props) {
65
+ const ctx = useScreen();
66
+ const { state } = useData();
67
+ const origName = typeof props.params.name === 'string' ? props.params.name : null;
68
+ const isNew = origName === null;
69
+ const [initial] = useState(() => {
70
+ const p = origName ? state.projects.find((x) => x.name === origName) : undefined;
71
+ return { name: origName ?? '', path: p?.path ?? '', base: p?.base ?? '' };
72
+ });
73
+ const [draft, setDraft] = useState(initial);
74
+ const fields = fieldsFor(isNew);
75
+ const [focus, setFocus] = useState(fields[0]);
76
+ const [attempted, setAttempted] = useState(false);
77
+ const [busy, setBusy] = useState(false);
78
+ const [error, setError] = useState(null);
79
+ const dirty = JSON.stringify(draft) !== JSON.stringify(initial);
80
+ const problems = projectProblems(draft, isNew, state.projects.map((p) => p.name));
81
+ const args = projectSaveArgs(draft, isNew);
82
+ const set = (key) => (value) => setDraft((d) => ({ ...d, [key]: value }));
83
+ const repo = repoState(draft.path);
84
+ useHints(KEY_HINTS);
85
+ useEquivalent(draft.name.trim() ? args : null);
86
+ useBackGuard(() => true);
87
+ const save = async () => {
88
+ if (busy)
89
+ return;
90
+ if (problems.length) {
91
+ setAttempted(true);
92
+ return;
93
+ }
94
+ setBusy(true);
95
+ setError(null);
96
+ const r = await ctx.actions.run({ args, title: 'Save project', noConfirm: true, done: `Saved ${draft.name.trim()}` });
97
+ setBusy(false);
98
+ if (r && r.code === 0)
99
+ ctx.nav.pop();
100
+ else if (r)
101
+ setError(r.output || `vsf exited with code ${r.code}`);
102
+ };
103
+ const discard = async () => {
104
+ if (dirty) {
105
+ const ok = await ctx.actions.confirm({ title: 'Discard changes?', body: 'What you typed here will be lost.', confirmLabel: 'Discard', cancelLabel: 'Keep editing' });
106
+ if (!ok)
107
+ return;
108
+ }
109
+ ctx.nav.pop();
110
+ };
111
+ useKeys((input, key) => {
112
+ if (key.escape)
113
+ void discard();
114
+ else if (key.ctrl && input === 's')
115
+ void save();
116
+ else if (key.tab)
117
+ setFocus((f) => moveFocus(fields, f, key.shift ? -1 : 1));
118
+ else if (key.downArrow)
119
+ setFocus((f) => moveFocus(fields, f, 1));
120
+ else if (key.upArrow)
121
+ setFocus((f) => moveFocus(fields, f, -1));
122
+ }, { whileCapturing: true });
123
+ const pathNote = repo === 'ok'
124
+ ? h(Text, { color: theme.green }, 'git repository')
125
+ : repo === 'not-repo'
126
+ ? h(Text, { color: theme.red }, 'Not a git repository (no .git in that folder)')
127
+ : h(Text, { color: theme.dim }, 'Where the repo lives, like ~/me/tether/tether-relay');
128
+ return h(Panel, { title: isNew ? 'New project' : 'Edit project', focused: true }, isNew
129
+ ? h(Row, { label: 'Name', focused: focus === 'name' }, h(TextInput, { value: draft.name, onChange: set('name'), focus: focus === 'name', placeholder: 'tether-relay' }))
130
+ : h(Row, { label: 'Name', focused: false }, h(Text, null, draft.name), h(Text, { color: theme.dim }, ' R on the list renames it')), h(Text, null, ' '), h(Row, { label: 'Path', focused: focus === 'path', note: pathNote }, h(TextInput, { value: draft.path, onChange: set('path'), focus: focus === 'path', placeholder: '~/me/repo' })), h(Text, null, ' '), h(Row, { label: 'Base branch', focused: focus === 'base', note: h(Text, { color: theme.dim }, 'Pull requests go into this branch. Empty means the repo default.') }, h(TextInput, { value: draft.base, onChange: set('base'), focus: focus === 'base', placeholder: 'default branch' })), attempted && problems.length ? h(Box, { flexDirection: 'column', marginTop: 1 }, ...problems.map((p, i) => h(Text, { key: i, color: theme.red }, `! ${p}`))) : null, error ? h(Box, { marginTop: 1 }, h(Text, { color: theme.red }, error)) : null, busy ? h(Text, { color: theme.dim }, 'Saving...') : null);
131
+ }
132
+ export const projectFormScreen = {
133
+ id: 'agents.project.form',
134
+ title: (p) => ['Projects', typeof p.name === 'string' ? `Edit ${p.name}` : 'New project'],
135
+ component: (props) => h(ProjectForm, { params: props.params }),
136
+ hints: () => KEY_HINTS,
137
+ };
@@ -0,0 +1,17 @@
1
+ import { type ProjectItem } from '../data.ts';
2
+ import { type Column } from '../widgets.ts';
3
+ import type { Screen } from '../screens.ts';
4
+ export declare const baseText: (p: Pick<ProjectItem, "base">) => string;
5
+ export declare const usedByText: (p: Pick<ProjectItem, "usedBy">) => string;
6
+ export declare function projectRows(projects: ProjectItem[]): (string | {
7
+ text: string;
8
+ bold: boolean;
9
+ colour?: undefined;
10
+ } | {
11
+ text: string;
12
+ colour: "#6b7280";
13
+ bold?: undefined;
14
+ })[][];
15
+ export declare function projectColumns(columns: number, projects?: ProjectItem[]): Column[];
16
+ export declare function deleteBlock(p: ProjectItem): string | null;
17
+ export declare const projectsScreen: Screen;
@@ -0,0 +1,109 @@
1
+ // Agents, Projects: the repos the workflows run in.
2
+ import React, { useState } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { useData } from "../data.js";
5
+ import { useEquivalent, useHints, useKeys, useScreen } from "../context.js";
6
+ import { tilde } from "../format.js";
7
+ import { theme } from "../theme.js";
8
+ import { Panel, Table, TextInput } from "../widgets.js";
9
+ const h = React.createElement;
10
+ export const baseText = (p) => p.base ?? 'default branch';
11
+ export const usedByText = (p) => (p.usedBy?.length ? `used by ${p.usedBy.join(', ')}` : 'not used');
12
+ export function projectRows(projects) {
13
+ return projects.map((p) => [{ text: p.name, bold: true }, tilde(p.path), `PRs into ${baseText(p)}`, { text: usedByText(p), colour: theme.dim }]);
14
+ }
15
+ export function projectColumns(columns, projects = []) {
16
+ const fit = (min, texts, max) => Math.min(max, Math.max(min, ...texts.map((t) => t.length + 2)));
17
+ const name = fit(12, projects.map((p) => p.name), 26);
18
+ const base = fit(18, projects.map((p) => `PRs into ${baseText(p)}`), 30);
19
+ const path = Math.min(fit(16, projects.map((p) => tilde(p.path)), 48), Math.max(16, columns - 12 - 2 - name - base - 24));
20
+ return [{ title: 'Name', width: name }, { title: 'Path', width: path }, { title: 'Base', width: base }, { title: 'Used by' }];
21
+ }
22
+ // Why a project cannot be deleted, or null when it can (the command refuses the same way).
23
+ export function deleteBlock(p) {
24
+ return p.usedBy?.length ? `${p.name} is used by ${p.usedBy.join(', ')}. Change or delete those workflows first.` : null;
25
+ }
26
+ const LIST_HINTS = [
27
+ { key: 'n', label: 'new' },
28
+ { key: 'e', label: 'edit' },
29
+ { key: 'R', label: 'rename' },
30
+ { key: 'd', label: 'delete' },
31
+ { key: 'esc', label: 'back' },
32
+ ];
33
+ function Projects() {
34
+ const ctx = useScreen();
35
+ const { state } = useData();
36
+ const list = state.projects;
37
+ const [selName, setSelName] = useState(null);
38
+ const [renaming, setRenaming] = useState(null);
39
+ const [draft, setDraft] = useState('');
40
+ const found = list.findIndex((p) => p.name === selName);
41
+ const index = found >= 0 ? found : Math.min(0, list.length - 1);
42
+ const selected = list[index];
43
+ useHints(renaming !== null ? [{ key: 'enter', label: 'rename' }, { key: 'esc', label: 'cancel' }] : LIST_HINTS);
44
+ useEquivalent(renaming !== null && draft.trim() && draft.trim() !== renaming ? ['agents', 'project', 'rename', renaming, draft.trim()] : null);
45
+ const rename = async () => {
46
+ const next = draft.trim();
47
+ if (renaming === null)
48
+ return;
49
+ if (!next || next === renaming) {
50
+ setRenaming(null);
51
+ return;
52
+ }
53
+ if (list.some((p) => p.name === next)) {
54
+ ctx.toast(`A project named ${next} already exists.`, 'error');
55
+ return;
56
+ }
57
+ const r = await ctx.actions.run({ args: ['agents', 'project', 'rename', renaming, next], title: 'Rename project', noConfirm: true, done: `Renamed to ${next}` });
58
+ if (r && r.code === 0)
59
+ setSelName(next);
60
+ if (r)
61
+ setRenaming(null);
62
+ };
63
+ useKeys((input, key) => {
64
+ if (renaming !== null) {
65
+ if (key.escape)
66
+ setRenaming(null);
67
+ return;
68
+ }
69
+ if (key.upArrow || input === 'k')
70
+ setSelName(list[Math.max(0, index - 1)]?.name ?? null);
71
+ else if (key.downArrow || input === 'j')
72
+ setSelName(list[Math.min(list.length - 1, index + 1)]?.name ?? null);
73
+ else if (input === 'n')
74
+ ctx.nav.push('agents.project.form', {});
75
+ else if ((input === 'e' || key.return) && selected)
76
+ ctx.nav.push('agents.project.form', { name: selected.name });
77
+ else if (input === 'R' && selected) {
78
+ setDraft(selected.name);
79
+ setRenaming(selected.name);
80
+ }
81
+ else if (input === 'd' && selected) {
82
+ const block = deleteBlock(selected);
83
+ if (block)
84
+ ctx.toast(block, 'error');
85
+ else
86
+ void ctx.actions.run({
87
+ args: ['agents', 'project', 'rm', selected.name, '--yes'],
88
+ title: `Delete project ${selected.name}?`,
89
+ body: 'Only the entry is removed. The repo on disk is not touched.',
90
+ confirmLabel: 'Delete',
91
+ done: `Deleted ${selected.name}`,
92
+ });
93
+ }
94
+ }, { whileCapturing: true });
95
+ return h(Panel, { title: 'Projects', focused: true }, h(Text, { color: theme.dim }, 'Repos on this Mac where agents do the work.'), h(Text, null, ' '), h(Table, {
96
+ columns: projectColumns(ctx.columns, list),
97
+ rows: projectRows(list),
98
+ selected: index,
99
+ empty: state.loaded ? 'No projects yet. Press n to add one.' : 'Loading...',
100
+ }), renaming !== null
101
+ ? h(Box, { marginTop: 1 }, h(Text, { color: theme.dim }, `Rename ${renaming} to `), h(TextInput, { value: draft, onChange: setDraft, onSubmit: () => void rename() }))
102
+ : null, state.errors.projects ? h(Text, { color: theme.red }, state.errors.projects) : null);
103
+ }
104
+ export const projectsScreen = {
105
+ id: 'agents.projects',
106
+ title: 'Projects',
107
+ component: Projects,
108
+ hints: () => LIST_HINTS,
109
+ };
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { type Key } from 'ink';
3
+ import { type ActionSpec, type Hint } from '../context.ts';
4
+ import { type DataState, type RunItem, type StepRecord } from '../data.ts';
5
+ import { type Line } from '../widgets.ts';
6
+ import type { Screen } from '../screens.ts';
7
+ export declare const manualLogPath: (dir: string) => string;
8
+ export declare const tabLock: {
9
+ on: boolean;
10
+ };
11
+ export declare function filterRuns(runs: RunItem[], text: string): RunItem[];
12
+ export declare function stepsLabel(steps: StepRecord[]): string;
13
+ export declare function roundsOf(steps: StepRecord[]): number;
14
+ export declare function runMs(run: RunItem, nowMs: number): number | null;
15
+ export declare function outcomeOf(run: RunItem, state: Pick<DataState, 'agents'>): string;
16
+ export declare function isLive(run: RunItem | undefined, state: Pick<DataState, 'agents'>): boolean;
17
+ export declare function startedText(run: RunItem): string;
18
+ export declare function settledSteps(run: RunItem, state: Pick<DataState, 'agents'>): StepRecord[];
19
+ export declare function stepMark(step: StepRecord): {
20
+ glyph: string;
21
+ colour: string;
22
+ };
23
+ export declare const stepResult: (step: StepRecord) => string;
24
+ export declare function stepMs(step: StepRecord, nowMs: number): number | null;
25
+ export declare const stepDuration: (step: StepRecord, nowMs: number) => string;
26
+ export declare function stepLine(step: StepRecord, nowMs: number): Line;
27
+ export declare function windowStart(total: number, selected: number, size: number, start: number): number;
28
+ export interface RunsKeys {
29
+ action: 'open' | 'cancel' | 'retry' | 'from' | 'clean' | 'filter' | 'up' | 'down' | null;
30
+ }
31
+ export declare function runsKey(input: string, key: Partial<Key>): RunsKeys['action'];
32
+ export declare function cancelSpec(card: string, stepLabel?: string): ActionSpec;
33
+ export declare const RUN_HINTS: Hint[];
34
+ export declare function logStepOf(run: RunItem): StepRecord | undefined;
35
+ export declare function fetchLabelsReal(card: string): Promise<string[] | null>;
36
+ export declare const runsDeps: {
37
+ fetchLabels: typeof fetchLabelsReal;
38
+ debounceMs: number;
39
+ };
40
+ export declare function Runs(props: {
41
+ params: Record<string, unknown>;
42
+ }): React.ReactElement;
43
+ export declare const runsScreen: Screen;