@fapost/jig-ui 0.1.0 → 0.2.0-next.21
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/dist/http/app.js +51 -33
- package/dist/jig/client.js +141 -139
- package/dist/registry/instance.js +1 -1
- package/dist/registry/projects.js +1 -1
- package/dist/source/local.js +29 -7
- package/dist/web/assets/CanvasView-4RWri2ak.js +1 -0
- package/dist/web/assets/index-D1V3u6PF.css +2 -0
- package/dist/web/assets/{index-CYfTN3Dj.js → index-DOnV4WX5.js} +2 -2
- package/dist/web/index.html +2 -2
- package/package.json +5 -3
- package/dist/web/assets/CanvasView-kgt4JQsV.js +0 -1
- package/dist/web/assets/index-hMczOUNS.css +0 -2
package/dist/http/app.js
CHANGED
|
@@ -4,7 +4,10 @@ import { isAbsolute, join } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { serveStatic } from '@hono/node-server/serve-static';
|
|
6
6
|
import { Hono } from 'hono';
|
|
7
|
+
import { HTTPException } from 'hono/http-exception';
|
|
8
|
+
import { secureHeaders } from 'hono/secure-headers';
|
|
7
9
|
import { streamSSE } from 'hono/streaming';
|
|
10
|
+
import { validator } from 'hono/validator';
|
|
8
11
|
import { columns, placeOf } from '../board/columns.js';
|
|
9
12
|
import { RegistryUnreadable } from '../source/project-source.js';
|
|
10
13
|
import { localOnly, tokenAccess } from './access.js';
|
|
@@ -29,61 +32,58 @@ function absolute(path) {
|
|
|
29
32
|
const expanded = path.trim().replace(/^~(?=$|[\\/])/, homedir());
|
|
30
33
|
return isAbsolute(expanded) ? expanded : undefined;
|
|
31
34
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
});
|
|
42
|
-
app.get('/api/instance', (c) => c.json({ version }));
|
|
43
|
-
app.get('/api/workspace', async (c) => c.json({ projects: await source.workspace() }));
|
|
44
|
-
app.get('/api/projects/:id/board', async (c) => {
|
|
35
|
+
/**
|
|
36
|
+
* The API, chained so that its type carries every route's answers: the page derives its types
|
|
37
|
+
* from `AppType` and never declares them a second time.
|
|
38
|
+
*/
|
|
39
|
+
function apiRoutes(source, version) {
|
|
40
|
+
return (new Hono()
|
|
41
|
+
.get('/api/instance', (c) => c.json({ version }, 200))
|
|
42
|
+
.get('/api/workspace', async (c) => c.json({ projects: await source.workspace() }, 200))
|
|
43
|
+
.get('/api/projects/:id/board', async (c) => {
|
|
45
44
|
const id = c.req.param('id');
|
|
46
45
|
const read = await source.tasks(id);
|
|
47
46
|
if (read === undefined)
|
|
48
47
|
return refuse(c, 404, 'not-connected', 'No connected project has this id.');
|
|
49
48
|
if (read.state !== 'ready')
|
|
50
|
-
return c.json({ board: read });
|
|
51
|
-
return c.json({ board: { id, state:
|
|
52
|
-
})
|
|
53
|
-
|
|
49
|
+
return c.json({ board: read }, 200);
|
|
50
|
+
return c.json({ board: { id, state: read.state, columns: columns(read.tasks), finished: read.finished } }, 200);
|
|
51
|
+
})
|
|
52
|
+
.get('/api/projects/:id/tasks/:task', async (c) => {
|
|
54
53
|
const read = await source.task(c.req.param('id'), c.req.param('task'));
|
|
55
54
|
if (read === undefined)
|
|
56
55
|
return refuse(c, 404, 'not-connected', 'No connected project has this id.');
|
|
57
56
|
if (read.state !== 'ready')
|
|
58
|
-
return c.json({ task: read });
|
|
57
|
+
return c.json({ task: read }, 200);
|
|
59
58
|
if (read.task === null)
|
|
60
59
|
return refuse(c, 404, 'not-found', 'This project has no live task with this id.');
|
|
61
|
-
return c.json({ task: { state:
|
|
62
|
-
})
|
|
63
|
-
|
|
60
|
+
return c.json({ task: { state: read.state, ...read.task, column: placeOf(read.task) } }, 200);
|
|
61
|
+
})
|
|
62
|
+
.get('/api/projects/:id/specs', async (c) => {
|
|
64
63
|
const read = await source.specs(c.req.param('id'));
|
|
65
64
|
if (read === undefined)
|
|
66
65
|
return refuse(c, 404, 'not-connected', 'No connected project has this id.');
|
|
67
|
-
return c.json({ specs: read });
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
return c.json({ specs: read }, 200);
|
|
67
|
+
})
|
|
68
|
+
.post('/api/projects', validator('json', (body) => ({
|
|
69
|
+
path: absolute(body?.path),
|
|
70
|
+
})), async (c) => {
|
|
71
|
+
const { path } = c.req.valid('json');
|
|
72
72
|
if (path === undefined)
|
|
73
73
|
return refuse(c, 422, 'usage', 'Give the absolute path of a project.');
|
|
74
74
|
const result = await source.connect(path);
|
|
75
75
|
if ('code' in result)
|
|
76
76
|
return refuse(c, 422, result.code, result.message);
|
|
77
|
-
return c.json({ project: result.project },
|
|
78
|
-
})
|
|
79
|
-
|
|
77
|
+
return result.created ? c.json({ project: result.project }, 201) : c.json({ project: result.project }, 200);
|
|
78
|
+
})
|
|
79
|
+
.delete('/api/projects/:id', async (c) => {
|
|
80
80
|
if (!(await source.disconnect(c.req.param('id')))) {
|
|
81
81
|
return refuse(c, 404, 'not-connected', 'No connected project has this id.');
|
|
82
82
|
}
|
|
83
83
|
return c.body(null, 204);
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
})
|
|
85
|
+
// A signal, not data: the page re-fetches what it shows (adr-20261014-project-source-port-keeps-hosting-open).
|
|
86
|
+
.get('/api/events', (c) => streamSSE(c, async (stream) => {
|
|
87
87
|
const unsubscribe = source.subscribe(({ id, revision }) => {
|
|
88
88
|
void stream.writeSSE({ event: 'changed', data: JSON.stringify({ project: id, revision }) });
|
|
89
89
|
});
|
|
@@ -93,7 +93,25 @@ export function createApp({ port, token, source, version = readPackageVersion(),
|
|
|
93
93
|
if (!stream.aborted)
|
|
94
94
|
await stream.write(': ping\n\n');
|
|
95
95
|
}
|
|
96
|
-
}));
|
|
96
|
+
})));
|
|
97
|
+
}
|
|
98
|
+
export function createApp({ port, token, source, version = readPackageVersion(), webRoot = DEFAULT_WEB_ROOT }) {
|
|
99
|
+
const app = new Hono();
|
|
100
|
+
// Before the guard, so its refusals carry them too; HSTS means nothing on http://127.0.0.1.
|
|
101
|
+
app.use(secureHeaders({ strictTransportSecurity: false }));
|
|
102
|
+
app.use(localOnly(port));
|
|
103
|
+
app.use(tokenAccess({ port, token }));
|
|
104
|
+
app.onError((error, c) => {
|
|
105
|
+
if (error instanceof RegistryUnreadable)
|
|
106
|
+
return refuse(c, 500, 'registry-unreadable', error.message);
|
|
107
|
+
// Hono's own refusals, such as a body that is no JSON: their status, in the API's form.
|
|
108
|
+
if (error instanceof HTTPException)
|
|
109
|
+
return c.json({ error: { code: 'usage', message: error.message } }, error.status);
|
|
110
|
+
// Only the terminal hears what failed: the message may name a path.
|
|
111
|
+
console.error(error);
|
|
112
|
+
return refuse(c, 500, 'internal', 'Jig UI hit an error it did not expect; its terminal says what.');
|
|
113
|
+
});
|
|
114
|
+
app.route('/', apiRoutes(source, version));
|
|
97
115
|
app.all('/api/*', (c) => c.json({ error: 'not found' }, 404));
|
|
98
116
|
if (existsSync(join(webRoot, 'index.html'))) {
|
|
99
117
|
app.use('/*', serveStatic({ root: webRoot }));
|
package/dist/jig/client.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFile } from 'node:child_process';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
|
+
import { z } from 'zod';
|
|
5
6
|
const run = promisify(execFile);
|
|
6
7
|
/** The oldest Jig whose reads Jig UI understands: 0.29.0 is the first that prints JSON. */
|
|
7
8
|
export const MIN_JIG_VERSION = '0.29.0';
|
|
@@ -17,6 +18,7 @@ export const watchDir = (root) => join(root, '.ai');
|
|
|
17
18
|
* (Jig 0.29.0, found by watching which files its reads touch).
|
|
18
19
|
*/
|
|
19
20
|
export const writtenByReads = (path) => (path === 'runtime' || path.startsWith('runtime/')) && !path.startsWith('runtime/revision');
|
|
21
|
+
let gitBash;
|
|
20
22
|
/**
|
|
21
23
|
* The bash that runs `.ai/scripts/jig`. On Windows it is Git's, found next to git itself
|
|
22
24
|
* (`<git>/mingw64/libexec/git-core` → `<git>/bin/bash.exe`): a `bash` on PATH may be WSL's.
|
|
@@ -24,8 +26,12 @@ export const writtenByReads = (path) => (path === 'runtime' || path.startsWith('
|
|
|
24
26
|
async function bash(platform) {
|
|
25
27
|
if (platform !== 'win32')
|
|
26
28
|
return 'bash';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
// Asked once per process; a failed ask is forgotten, so the next run asks again.
|
|
30
|
+
gitBash ??= run('git', ['--exec-path'], { timeout: TIMEOUT_MS }).then(({ stdout }) => resolve(stdout.trim(), '..', '..', '..', 'bin', 'bash.exe'), (error) => {
|
|
31
|
+
gitBash = undefined;
|
|
32
|
+
throw error;
|
|
33
|
+
});
|
|
34
|
+
return gitBash;
|
|
29
35
|
}
|
|
30
36
|
/** Runs the project's own Jig in its main checkout, without a shell. */
|
|
31
37
|
async function jig(root, args, platform) {
|
|
@@ -60,8 +66,6 @@ export class JigShapeError extends Error {
|
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
68
|
const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
63
|
-
const isString = (v) => typeof v === 'string';
|
|
64
|
-
const isNullableString = (v) => v === null || typeof v === 'string';
|
|
65
69
|
/**
|
|
66
70
|
* Runs a `--json` read and returns its `data` and the clone's revision. Jig's refusal throws
|
|
67
71
|
* JigRefusal, as does output that is no JSON at all (a crash); an envelope of another shape throws
|
|
@@ -87,159 +91,157 @@ async function readJson(root, args, platform) {
|
|
|
87
91
|
throw new JigShapeError();
|
|
88
92
|
if (isObject(envelope.error)) {
|
|
89
93
|
const code = envelope.error.code;
|
|
90
|
-
throw new JigRefusal(
|
|
94
|
+
throw new JigRefusal(typeof code === 'string' && CODES.has(code) ? code : 'failed');
|
|
91
95
|
}
|
|
92
96
|
if (!isObject(envelope.data))
|
|
93
97
|
throw new JigShapeError();
|
|
94
98
|
return { revision: envelope.revision, data: envelope.data };
|
|
95
99
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
/** `data` read through `schema`, the shape its contract test pins; anything else is a Jig this Jig UI does not read. */
|
|
101
|
+
function parsed(schema, data) {
|
|
102
|
+
const result = schema.safeParse(data);
|
|
103
|
+
if (!result.success)
|
|
104
|
+
throw new JigShapeError();
|
|
105
|
+
return result.data;
|
|
106
|
+
}
|
|
107
|
+
// Each shape of Jig's output once: what Jig prints (snake_case, every field Jig UI reads present —
|
|
108
|
+
// `null` where Jig has nothing, never left out), turned into what Jig UI's types say. Keys Jig adds
|
|
109
|
+
// without raising `schema` are dropped.
|
|
110
|
+
const text = z.string();
|
|
111
|
+
const maybe = z.string().nullable();
|
|
112
|
+
const count = z.int().nonnegative();
|
|
113
|
+
/** Something that needs the person, as `jig status --json` reports it; a kind this Jig UI does not know is shown by its texts. */
|
|
114
|
+
const Attention = z.object({
|
|
115
|
+
kind: text,
|
|
116
|
+
subject: z.object({ type: text, id: maybe }),
|
|
117
|
+
tasks: z.array(text),
|
|
118
|
+
title: text,
|
|
119
|
+
detail: maybe,
|
|
120
|
+
todo: text,
|
|
121
|
+
url: maybe,
|
|
122
|
+
});
|
|
123
|
+
const Status = z.object({
|
|
124
|
+
attention: z.array(Attention),
|
|
125
|
+
housekeeping: z.object({ stale: z.boolean() }).nullable(),
|
|
126
|
+
});
|
|
107
127
|
/** What needs the person in a project, most urgent first, and whether the pull request facts behind it may be missing. */
|
|
108
128
|
export async function readStatus(root, platform = process.platform) {
|
|
109
129
|
const { revision, data } = await readJson(root, ['status'], platform);
|
|
110
|
-
const { attention, housekeeping } = data;
|
|
111
|
-
|
|
112
|
-
throw new JigShapeError();
|
|
113
|
-
if (housekeeping !== null && !(isObject(housekeeping) && typeof housekeeping.stale === 'boolean'))
|
|
114
|
-
throw new JigShapeError();
|
|
115
|
-
return {
|
|
116
|
-
revision,
|
|
117
|
-
attention: attention.map(({ kind, subject, tasks, title, detail, todo, url }) => ({
|
|
118
|
-
kind,
|
|
119
|
-
subject: { type: subject.type, id: subject.id },
|
|
120
|
-
tasks: [...tasks],
|
|
121
|
-
title,
|
|
122
|
-
detail,
|
|
123
|
-
todo,
|
|
124
|
-
url,
|
|
125
|
-
})),
|
|
126
|
-
housekeepingStale: housekeeping?.stale === true,
|
|
127
|
-
};
|
|
130
|
+
const { attention, housekeeping } = parsed(Status, data);
|
|
131
|
+
return { revision, attention, housekeepingStale: housekeeping?.stale === true };
|
|
128
132
|
}
|
|
129
|
-
const STATUSES = new Set(['active', 'ready', 'consolidated', 'abandoned']);
|
|
130
133
|
/** Jig's action ids this Jig UI knows; Jig may add ids without raising `schema`, and a consumer ignores those. */
|
|
131
134
|
const ACTIONS = new Set(['start', 'pause', 'resume', 'approve-gate', 'answer-stop', 'set-class', 'ship', 'close', 'abandon', 'reopen']);
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
135
|
+
/** What a person may do next with a task, as Jig decides it from the task's state. */
|
|
136
|
+
const TaskAction = z.object({
|
|
137
|
+
id: text,
|
|
138
|
+
/** The Jig command that takes it; a word in `<…>` is a value the person supplies. */
|
|
139
|
+
command: text,
|
|
140
|
+
offered: z.boolean(),
|
|
141
|
+
/** Jig's refusal, word for word, when not offered; it may name a path. */
|
|
142
|
+
reason: maybe,
|
|
143
|
+
});
|
|
144
|
+
/** A task as `jig task list --json` reports it, as far as Jig UI reads it; the statuses are Jig's (schemas/state.md). */
|
|
145
|
+
const TaskRow = z
|
|
146
|
+
.object({
|
|
147
|
+
id: text,
|
|
148
|
+
class: maybe,
|
|
149
|
+
status: z.enum(['active', 'ready', 'consolidated', 'abandoned']),
|
|
150
|
+
issue: maybe,
|
|
151
|
+
goal: maybe,
|
|
152
|
+
knowledge_consolidated: z.boolean(),
|
|
153
|
+
paused: z.boolean(),
|
|
154
|
+
created_at: maybe,
|
|
155
|
+
updated_at: maybe,
|
|
156
|
+
/** `null` while the task has not been started. */
|
|
157
|
+
branch: maybe,
|
|
158
|
+
base_branch: text,
|
|
159
|
+
/** The worktree that has the task's branch checked out: only how many files differ there, never where it is. */
|
|
160
|
+
worktree: z.object({ uncommitted: z.number() }).nullable(),
|
|
161
|
+
gate: z.object({ state: text, by: maybe }).nullable(),
|
|
162
|
+
autopilot: z
|
|
163
|
+
.object({ state: text, mode: maybe, stage: maybe, stage_at: maybe, stop_reason: maybe, stop_at: maybe, phase: maybe })
|
|
164
|
+
.nullable(),
|
|
165
|
+
pull_request: z.object({ url: text, state: maybe, ci: maybe, fetched_at: maybe }).nullable(),
|
|
166
|
+
housekeeping_flags: z.array(text),
|
|
167
|
+
actions: z.array(TaskAction),
|
|
168
|
+
})
|
|
169
|
+
.transform(({ autopilot: run, pull_request: pr, ...t }) => ({
|
|
170
|
+
id: t.id,
|
|
171
|
+
class: t.class,
|
|
172
|
+
status: t.status,
|
|
173
|
+
issue: t.issue,
|
|
174
|
+
goal: t.goal,
|
|
175
|
+
knowledgeConsolidated: t.knowledge_consolidated,
|
|
176
|
+
paused: t.paused,
|
|
177
|
+
createdAt: t.created_at,
|
|
178
|
+
updatedAt: t.updated_at,
|
|
179
|
+
branch: t.branch,
|
|
180
|
+
baseBranch: t.base_branch,
|
|
181
|
+
worktree: t.worktree,
|
|
182
|
+
gate: t.gate,
|
|
183
|
+
autopilot: run && {
|
|
184
|
+
state: run.state,
|
|
185
|
+
mode: run.mode,
|
|
186
|
+
stage: run.stage,
|
|
187
|
+
stageAt: run.stage_at,
|
|
188
|
+
stopReason: run.stop_reason,
|
|
189
|
+
stopAt: run.stop_at,
|
|
190
|
+
phase: run.phase,
|
|
191
|
+
},
|
|
192
|
+
pullRequest: pr && { url: pr.url, state: pr.state, ci: pr.ci, fetchedAt: pr.fetched_at },
|
|
193
|
+
housekeepingFlags: t.housekeeping_flags,
|
|
194
|
+
actions: t.actions.filter((a) => ACTIONS.has(a.id)),
|
|
195
|
+
}));
|
|
196
|
+
const TaskList = z.object({ tasks: z.array(TaskRow), hidden: z.number() });
|
|
184
197
|
/** The project's live tasks (`jig task list --json`) and how many finished ones it left out. */
|
|
185
198
|
export async function readTasks(root, platform = process.platform) {
|
|
186
199
|
const { revision, data } = await readJson(root, ['task', 'list'], platform);
|
|
187
|
-
|
|
188
|
-
throw new JigShapeError();
|
|
189
|
-
return { revision, tasks: data.tasks.map(summary), hidden: data.hidden };
|
|
200
|
+
return { revision, ...parsed(TaskList, data) };
|
|
190
201
|
}
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
202
|
+
/** How far a roadmap or one of its phases is: items checked, all items, items with a task filed, fog items. */
|
|
203
|
+
const SpecProgress = z.object({ done: count, total: count, filed: count, fog: count });
|
|
204
|
+
/** A roadmap item: its words as the roadmap has them, and the task it names, if any. */
|
|
205
|
+
const SpecItem = z.object({ text, checked: z.boolean(), task: maybe, fog: z.boolean() });
|
|
206
|
+
/** A roadmap phase; `phase` and `title` are `null` for items before any phase heading. */
|
|
207
|
+
const SpecPhase = SpecProgress.extend({ phase: count.nullable(), title: maybe, items: z.array(SpecItem) });
|
|
208
|
+
/** A spec as `jig spec list --json` reports it; the roadmap is read from the epic while one is open. */
|
|
209
|
+
const SpecRow = z
|
|
210
|
+
.object({
|
|
211
|
+
id: text,
|
|
212
|
+
title: maybe,
|
|
213
|
+
/** The spec's own files that are not there: `spec.md`, `roadmap.md`. */
|
|
214
|
+
missing_files: z.array(text),
|
|
215
|
+
/** The version level the epic's release raises, as the roadmap records it. */
|
|
216
|
+
release: maybe,
|
|
217
|
+
epic: z
|
|
218
|
+
.object({ branch: text, state: z.enum(['open', 'finished']), checked_out: z.boolean(), missing: z.boolean(), ref: maybe })
|
|
219
|
+
.nullable(),
|
|
220
|
+
/** `null` when the roadmap could not be read. */
|
|
221
|
+
progress: SpecProgress.nullable(),
|
|
222
|
+
phases: z.array(SpecPhase),
|
|
223
|
+
})
|
|
224
|
+
.transform(({ missing_files, epic, ...s }) => ({
|
|
225
|
+
id: s.id,
|
|
226
|
+
title: s.title,
|
|
227
|
+
missingFiles: missing_files,
|
|
228
|
+
release: s.release,
|
|
229
|
+
// `fetched`: the roadmap was read from the remote branch as of the last fetch; `missing`: the branch is nowhere.
|
|
230
|
+
epic: epic && {
|
|
231
|
+
branch: epic.branch,
|
|
232
|
+
state: epic.state,
|
|
233
|
+
checkedOut: epic.checked_out,
|
|
234
|
+
missing: epic.missing,
|
|
235
|
+
fetched: epic.ref?.startsWith('refs/remotes/') === true,
|
|
236
|
+
},
|
|
237
|
+
progress: s.progress,
|
|
238
|
+
phases: s.phases,
|
|
239
|
+
}));
|
|
240
|
+
const SpecList = z.object({ specs: z.array(SpecRow) });
|
|
218
241
|
/** The project's specs (`jig spec list --json`), in Jig's order. */
|
|
219
242
|
export async function readSpecs(root, platform = process.platform) {
|
|
220
243
|
const { data } = await readJson(root, ['spec', 'list'], platform);
|
|
221
|
-
|
|
222
|
-
throw new JigShapeError();
|
|
223
|
-
return data.specs.map((s) => ({
|
|
224
|
-
id: s.id,
|
|
225
|
-
title: s.title,
|
|
226
|
-
missingFiles: [...s.missing_files],
|
|
227
|
-
release: s.release,
|
|
228
|
-
epic: s.epic && {
|
|
229
|
-
branch: s.epic.branch,
|
|
230
|
-
state: s.epic.state,
|
|
231
|
-
checkedOut: s.epic.checked_out,
|
|
232
|
-
missing: s.epic.missing,
|
|
233
|
-
fetched: s.epic.ref?.startsWith('refs/remotes/') === true,
|
|
234
|
-
},
|
|
235
|
-
progress: s.progress && progress(s.progress),
|
|
236
|
-
phases: s.phases.map((p) => ({
|
|
237
|
-
...progress(p),
|
|
238
|
-
phase: p.phase,
|
|
239
|
-
title: p.title,
|
|
240
|
-
items: p.items.map(({ text, checked, task, fog }) => ({ text, checked, task, fog })),
|
|
241
|
-
})),
|
|
242
|
-
}));
|
|
244
|
+
return parsed(SpecList, data).specs;
|
|
243
245
|
}
|
|
244
246
|
/** True when `version` is at least the minimum Jig UI reads. */
|
|
245
247
|
export function supports(version, minimum = MIN_JIG_VERSION) {
|
|
@@ -57,7 +57,7 @@ export async function connectThrough(info, path, timeoutMs = 15_000) {
|
|
|
57
57
|
body: JSON.stringify({ path }),
|
|
58
58
|
signal: AbortSignal.timeout(timeoutMs),
|
|
59
59
|
});
|
|
60
|
-
//
|
|
60
|
+
// An instance before 0.2.0 answers a failure it did not foresee in plain text.
|
|
61
61
|
const body = (await res.json().catch(() => ({})));
|
|
62
62
|
if (body.project)
|
|
63
63
|
return { id: body.project.id, created: res.status === 201 };
|
|
@@ -34,7 +34,7 @@ export async function readProjects(dir) {
|
|
|
34
34
|
export function slugFor(root, taken) {
|
|
35
35
|
const base = basename(root)
|
|
36
36
|
.normalize('NFKD')
|
|
37
|
-
.replace(/
|
|
37
|
+
.replace(/\p{M}/gu, '')
|
|
38
38
|
.toLowerCase()
|
|
39
39
|
.replace(/[^a-z0-9]+/g, '-')
|
|
40
40
|
.replace(/^-+|-+$/g, '') || 'project';
|
package/dist/source/local.js
CHANGED
|
@@ -129,15 +129,19 @@ export function localSource(configDir, { rescanMs = 60_000, quietMs, maxWaitMs }
|
|
|
129
129
|
/** Counts the times following started; a read that finishes after a stop belongs to none. */
|
|
130
130
|
let session = 0;
|
|
131
131
|
const following = () => rescan !== undefined;
|
|
132
|
+
/** The ids the last sweep of this following found registered; none before its first. */
|
|
133
|
+
let known;
|
|
132
134
|
const entryOf = (project) => {
|
|
133
135
|
let entry = entries.get(project.id);
|
|
134
136
|
if (!entry)
|
|
135
|
-
entries.set(project.id, (entry = { project, held: false, dirty: false }));
|
|
137
|
+
entries.set(project.id, (entry = { project, held: false, dirty: false, news: false }));
|
|
136
138
|
return entry;
|
|
137
139
|
};
|
|
138
140
|
/** Keeps what was shown over one torn read; otherwise the read is what the project shows. */
|
|
139
141
|
function settle(entry, read) {
|
|
140
|
-
|
|
142
|
+
// While following, only a read this following showed is kept: one from before it was never shown through it.
|
|
143
|
+
const ours = !following() || entry.readIn === session;
|
|
144
|
+
if (torn(read) && entry.shown && !torn(entry.shown) && !entry.held && ours) {
|
|
141
145
|
entry.held = true;
|
|
142
146
|
return;
|
|
143
147
|
}
|
|
@@ -145,8 +149,12 @@ export function localSource(configDir, { rescanMs = 60_000, quietMs, maxWaitMs }
|
|
|
145
149
|
entry.shown = read;
|
|
146
150
|
entry.readIn = following() ? session : undefined;
|
|
147
151
|
}
|
|
148
|
-
/**
|
|
149
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Reads the project again — one read at a time — and signals when what it shows changed. The
|
|
154
|
+
* first read since following started signals nothing: every page reads when its stream opens.
|
|
155
|
+
*/
|
|
156
|
+
function refresh(entry, news = false) {
|
|
157
|
+
entry.news ||= news;
|
|
150
158
|
if (entry.reading) {
|
|
151
159
|
entry.dirty = true;
|
|
152
160
|
return entry.reading;
|
|
@@ -154,9 +162,16 @@ export function localSource(configDir, { rescanMs = 60_000, quietMs, maxWaitMs }
|
|
|
154
162
|
entry.reading = (async () => {
|
|
155
163
|
do {
|
|
156
164
|
entry.dirty = false;
|
|
165
|
+
const was = entry.readIn;
|
|
157
166
|
const before = JSON.stringify(entry.shown);
|
|
158
167
|
settle(entry, await readProject(entry.project));
|
|
159
|
-
|
|
168
|
+
// Compared only with a read of this following, checked once the read is in: one from before
|
|
169
|
+
// it, or from a following that ended meanwhile, was never shown through this one.
|
|
170
|
+
const changed = following() && was === session && JSON.stringify(entry.shown) !== before;
|
|
171
|
+
// News lasts one read: one made while nobody listens tells nobody, and must not tell the next listener.
|
|
172
|
+
const news = entry.news;
|
|
173
|
+
entry.news = false;
|
|
174
|
+
if (following() && entries.get(entry.project.id) === entry && (news || changed)) {
|
|
160
175
|
signal({ id: entry.project.id, revision: entry.shown?.revision ?? null });
|
|
161
176
|
}
|
|
162
177
|
} while (entry.dirty);
|
|
@@ -194,6 +209,8 @@ export function localSource(configDir, { rescanMs = 60_000, quietMs, maxWaitMs }
|
|
|
194
209
|
if (!projects || !following())
|
|
195
210
|
return;
|
|
196
211
|
const ids = new Set(projects.map((p) => p.id));
|
|
212
|
+
const before = known;
|
|
213
|
+
known = ids;
|
|
197
214
|
for (const [id, entry] of entries) {
|
|
198
215
|
if (ids.has(id))
|
|
199
216
|
continue;
|
|
@@ -203,11 +220,12 @@ export function localSource(configDir, { rescanMs = 60_000, quietMs, maxWaitMs }
|
|
|
203
220
|
for (const project of projects) {
|
|
204
221
|
const entry = entryOf(project);
|
|
205
222
|
follow(entry); // a watcher that failed, or a directory that came back
|
|
206
|
-
void refresh(entry);
|
|
223
|
+
void refresh(entry, before !== undefined && !before.has(project.id)); // one another Jig UI connected
|
|
207
224
|
}
|
|
208
225
|
}
|
|
209
226
|
function start() {
|
|
210
227
|
session++;
|
|
228
|
+
known = undefined;
|
|
211
229
|
rescan = setInterval(() => void sweep(), rescanMs);
|
|
212
230
|
rescan.unref();
|
|
213
231
|
void sweep();
|
|
@@ -235,7 +253,10 @@ export function localSource(configDir, { rescanMs = 60_000, quietMs, maxWaitMs }
|
|
|
235
253
|
const entry = entryOf(project);
|
|
236
254
|
if (following())
|
|
237
255
|
follow(entry);
|
|
238
|
-
|
|
256
|
+
// News too when another Jig UI registered it since the last sweep: this page has not heard of it.
|
|
257
|
+
const news = created || (known !== undefined && !known.has(project.id));
|
|
258
|
+
known?.add(project.id);
|
|
259
|
+
await refresh(entry, news);
|
|
239
260
|
return { project: entry.shown.view, created };
|
|
240
261
|
},
|
|
241
262
|
async tasks(id) {
|
|
@@ -260,6 +281,7 @@ export function localSource(configDir, { rescanMs = 60_000, quietMs, maxWaitMs }
|
|
|
260
281
|
return false;
|
|
261
282
|
entries.get(id)?.watch?.close();
|
|
262
283
|
entries.delete(id);
|
|
284
|
+
known?.delete(id); // connected again before the next sweep, it is news again
|
|
263
285
|
if (following())
|
|
264
286
|
signal({ id, revision: null });
|
|
265
287
|
return true;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{A as e,B as t,C as n,D as r,E as i,F as a,G as o,H as s,I as c,J as l,K as u,L as d,M as ee,N as te,O as f,P as p,Q as m,R as h,S as ne,T as re,U as g,V as _,W as v,X as y,Y as b,Z as x,_ as S,a as ie,b as C,c as ae,d as w,f as oe,g as T,h as E,i as D,j as O,k,l as se,m as ce,n as A,o as j,p as le,q as M,r as ue,s as de,t as fe,u as pe,v as me,w as he,x as N,y as P,z as F}from"./index-DOnV4WX5.js";var I=s({__name:`BaseSeparator`,props:{orientation:{type:String,required:!1,default:`horizontal`},decorative:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},setup(e){let t=e,n=[`horizontal`,`vertical`];function r(e){return n.includes(e)}let i=a(()=>r(t.orientation)?t.orientation:`horizontal`),s=a(()=>i.value===`vertical`?t.orientation:void 0),c=a(()=>t.decorative?{role:`none`}:{"aria-orientation":s.value,role:`separator`});return(t,n)=>(o(),d(b(f),v({as:e.as,"as-child":e.asChild,"data-orientation":i.value},c.value),{default:l(()=>[M(t.$slots,`default`)]),_:3},16,[`as`,`as-child`,`data-orientation`]))}}),L=s({__name:`Separator`,props:{orientation:{type:String,required:!1,default:`horizontal`},decorative:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},setup(e){let t=e;return(e,n)=>(o(),d(I,x(g(t)),{default:l(()=>[M(e.$slots,`default`)]),_:3},16))}}),R=s({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(e){let t=e;return(e,n)=>(o(),F(`div`,{"data-slot":`card-content`,class:y(b(k)(`px-4 group-data-[size=sm]/card:px-3`,t.class))},[M(e.$slots,`default`)],2))}}),ge=s({__name:`CardDescription`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(e){let t=e;return(e,n)=>(o(),F(`div`,{"data-slot":`card-description`,class:y(b(k)(`text-xs/relaxed text-muted-foreground`,t.class))},[M(e.$slots,`default`)],2))}}),_e=s({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(e){let t=e;return(e,n)=>(o(),F(`div`,{"data-slot":`card-header`,class:y(b(k)(`group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-none px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3`,t.class))},[M(e.$slots,`default`)],2))}}),ve=s({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(e){let t=e;return(e,n)=>(o(),F(`div`,{"data-slot":`card-title`,class:y(b(k)(`cn-font-heading text-sm font-medium group-data-[size=sm]/card:text-sm`,t.class))},[M(e.$slots,`default`)],2))}}),z=s({__name:`Separator`,props:{orientation:{default:`horizontal`},decorative:{type:Boolean,default:!0},asChild:{type:Boolean},as:{},class:{type:[Boolean,null,String,Object,Array]}},setup(e){let t=e,n=he(t,`class`);return(e,r)=>(o(),d(b(L),v({"data-slot":`separator`},b(n),{class:b(k)(`shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch`,t.class)}),null,16,[`class`]))}}),ye=[`aria-label`],be={class:`flex items-baseline gap-3 border-b bg-muted/50 px-4 py-2`},xe={class:`font-semibold tracking-wider uppercase`},Se={key:0,class:`text-muted-foreground`},Ce={class:`grid grid-cols-1 divide-y lg:grid-cols-2 lg:divide-x lg:divide-y-0`},B=s({__name:`CanvasSection`,props:{title:{},note:{}},setup(e){return(t,n)=>(o(),F(`section`,{"aria-label":e.title,class:`border-b`},[c(`h2`,be,[c(`span`,xe,m(e.title),1),e.note?(o(),F(`span`,Se,m(e.note),1)):h(``,!0)]),c(`div`,Ce,[(o(),F(p,null,u([`light`,`dark`],e=>c(`div`,{key:e,class:y([e,`flex flex-col gap-4 bg-background p-4 text-foreground`])},[M(t.$slots,`default`)],2)),64))])],8,ye))}}),V=(e,t)=>({id:e,class:`T2`,status:`active`,goal:null,paused:!1,started:!0,gate:null,autopilot:null,pullRequest:null,...t}),H=(e,t)=>({url:`https://github.com/fapost-lab/jig-ui/pull/18`,state:e,ci:t,fetchedAt:`2026-10-18T09:40:00Z`}),U=[V(`design-system`,{class:`T3`,gate:`waiting`,goal:`shadcn-vue on Tailwind v4 with light and dark themes; #/canvas with every component in every variant; the three screens rebuilt on it.`}),V(`registry-schema`,{gate:`changed`,goal:`Version the registry file so a newer Jig UI can read an older one.`}),V(`pause-request`,{class:`T3`,autopilot:`stopped`,goal:`A drop on the paused column writes a sticky request through Jig; the card shows it with its age and a cancel.`}),V(`specs-and-epics`,{autopilot:`on`,goal:`Show each spec with its phases, progress and epic branch beside the board, from Jig's spec list.`}),V(`task-details`,{status:`ready`,pullRequest:H(`open`,`green`),goal:`Open a task's details from its card.`}),V(`refusal-fixture`,{class:`T0`,status:`ready`,pullRequest:H(`open`,`red`),goal:`Replace the refusal text in task-list-states.json with one Jig prints.`}),V(`docs-board`,{class:`T1`,status:`ready`,pullRequest:H(`open`,`pending`),goal:`Describe the five columns on the projects page of the docs.`}),V(`live-updates`,{status:`ready`,pullRequest:H(`merged`,`none`),goal:`Follow connected projects without a reload.`}),V(`no-pr-yet`,{class:`T1`,status:`ready`,goal:`Ready, nothing pushed.`}),V(`jig-init-hook`,{class:`T3`,paused:!0,goal:`Register a project in an installed Jig UI from jig init.`}),V(`file-backlog-task`,{class:null,started:!1,goal:`File a minimal backlog task from the page through jig task new.`}),V(`a-task-with-a-rather-long-identifier-that-wraps`,{goal:null})],W=[{id:`needs-you`,title:`Needs you`,cards:U.slice(0,3)},{id:`in-progress`,title:`In progress`,cards:[U[3],U[11]]},{id:`ready`,title:`Ready`,cards:U.slice(4,9)},{id:`backlog`,title:`Backlog`,cards:[U[10]]},{id:`paused`,title:`Paused`,cards:[U[9]]}],G=W.map(e=>({...e,cards:[]})),we=[{id:`needs-you`,title:`Needs you`,count:3},{id:`in-progress`,title:`In progress`,count:1},{id:`ready`,title:`Ready`,count:5},{id:`backlog`,title:`Backlog`,count:1},{id:`paused`,title:`Paused`,count:1},{id:`unknown`,title:`A column this page does not know`,count:0}],K=[{kind:`gate`,subject:{type:`task`,id:`design-system`},tasks:[`design-system`],title:`Design waits at its gate`,detail:null,todo:`Review the design and approve it: jig task gate design-system approved`,url:null},{kind:`pull-request-ci-failed`,subject:{type:`task`,id:`refusal-fixture`},tasks:[`refusal-fixture`],title:`CI failed`,detail:`pull request #19`,todo:`Look at the failing check.`,url:`https://github.com/fapost-lab/jig-ui/pull/19`},{kind:`housekeeping`,subject:{type:`project`,id:null},tasks:[],title:`Housekeeping never ran`,detail:null,todo:`Run jig housekeeping.`,url:`https://example.com/docs`}],q=[{id:`approve-gate`,command:`jig task gate design-system approved`},{id:`answer-stop`,command:`jig task autopilot pause-request resume --answer <text>`},{id:`pause`,command:`jig task pause design-system`},{id:`something-new`,command:`jig task something-new design-system`}],J={state:`ready`,issue:null,baseBranch:`main`,createdAt:`2026-10-17`,updatedAt:`2026-10-18`,knowledgeConsolidated:!1,worktree:null,gateBy:null,run:null,housekeepingFlags:[],attention:[]},Y=[{...J,...U[2],gate:`approved`,gateBy:`human`,branch:`task/pause-request`,worktree:{uncommitted:4},issue:`#42`,pullRequest:H(`open`,`red`),run:{mode:`attended`,stage:`implement`,stageAt:`2026-10-18T09:00:00Z`,stopReason:`Which column takes a paused task that already has an open pull request?`,stopAt:`2026-10-18T09:12:00Z`,phase:null},housekeepingFlags:[`needs-consolidation`],attention:[K[0]],actions:q,column:{id:`needs-you`,title:`Needs you`}},{...J,...U[10],branch:null,actions:[{id:`start`,command:`jig task start file-backlog-task`}],column:{id:`backlog`,title:`Backlog`}}],X=(e,t={})=>({id:e,state:`ready`,jigVersion:`0.29.0`,counts:{active:11,paused:1,ready:3},attention:[],housekeepingStale:!1,...t}),Z=[X(`jig-ui`,{attention:K,housekeepingStale:!0}),X(`nextjs-store`,{counts:{active:0,paused:0,ready:0}}),{id:`old-jig`,state:`unsupported`,jigVersion:`0.26.0`,minimum:`0.29.0`},{id:`new-jig`,state:`incompatible`,jigVersion:`1.0.0`},{id:`busy`,state:`unreadable`,code:`locked`},{id:`moved`,state:`unavailable`,reason:`missing`}],Q=[{text:"`design-system` — design system and canvas",checked:!0,task:`design-system`,fog:!1},{text:"`specs-and-epics` — specs and epics (after: project board — `jig spec list`)",checked:!1,task:`specs-and-epics`,fog:!1},{text:"`old-task` — a task no longer live",checked:!1,task:`old-task`,fog:!1},{text:`invoices from orders, no task filed yet`,checked:!1,task:null,fog:!1},{text:"fog: registration by `jig init`",checked:!1,task:null,fog:!0}],Te=[{id:`jig-ui`,title:`Jig UI — a local control center for Jig projects`,missingFiles:[],release:null,epic:null,progress:{done:2,total:6,filed:2,fog:1},phases:[{phase:0,title:`Package and release`,done:1,total:1,filed:0,fog:0,items:[{...Q[0],text:`package and release`,task:null}]},{phase:1,title:`Read-only UI`,done:1,total:5,filed:2,fog:1,items:Q}]},{id:`billing`,title:`Billing in one place`,missingFiles:[],release:`minor`,epic:{branch:`epic/billing`,state:`open`,checkedOut:!1,missing:!1,fetched:!0},progress:{done:0,total:1,filed:0,fog:0},phases:[{phase:null,title:null,done:0,total:1,filed:0,fog:0,items:[Q[3]]}]},{id:`lost-epic`,title:`An epic whose branch is gone`,missingFiles:[],release:`major`,epic:{branch:`epic/lost`,state:`open`,checkedOut:!0,missing:!0,fetched:!1},progress:null,phases:[]},{id:`half-written`,title:null,missingFiles:[`spec.md`,`roadmap.md`],release:null,epic:null,progress:null,phases:[]}],Ee={class:`flex gap-4`},De={class:`w-24 text-muted-foreground`},Oe={class:`flex items-center gap-2`},ke={class:`flex flex-wrap gap-2`},Ae={class:`flex max-w-sm flex-col gap-2`},$={class:`flex h-5 items-center gap-2`},je={class:`grid grid-cols-2 gap-2 xl:grid-cols-3`},Me={class:`flex h-[28rem] flex-col`},Ne={class:`flex h-40 flex-col`},Pe={class:`flex h-[28rem] flex-col`},Fe={class:`flex h-64 flex-col`},Ie={class:`flex h-64 flex-col`},Le=s({__name:`CanvasView`,setup(a){let s=[`default`,`secondary`,`outline`,`ghost`,`destructive`,`link`],f=[`xs`,`sm`,`default`,`lg`],h=[`icon-xs`,`icon-sm`,`icon`,`icon-lg`],g=[`default`,`secondary`,`outline`,`ghost`,`destructive`,`link`],v=[`attention`,`good`,`bad`,`quiet`];return(a,x)=>(o(),F(`div`,null,[x[12]||=c(`div`,{class:`border-b px-4 py-3`},[c(`h1`,{class:`font-semibold tracking-wider uppercase`},`Canvas`),c(`p`,{class:`text-muted-foreground`},`Every component of the page in every variant and state; the left panel light, the right dark.`)],-1),_(B,{title:`Tones`,note:`lib/tones.ts — the only colours with a meaning`},{default:l(()=>[(o(),F(p,null,u(v,e=>c(`p`,{key:e,class:y(b(ee)[e])},m(e)+` as a rule`,3)),64)),c(`p`,Ee,[(o(),F(p,null,u(v,e=>c(`span`,{key:e,class:y(b(te)[e])},m(e),3)),64))])]),_:1}),x[13]||=c(`h2`,{class:`border-b px-4 py-2 font-bold tracking-widest uppercase`},`components/ui`,-1),_(B,{title:`Button`,note:`variant × size; icon sizes; disabled`},{default:l(()=>[(o(),F(p,null,u(s,e=>c(`div`,{key:e,class:`flex flex-wrap items-center gap-2`},[c(`span`,De,m(e),1),(o(),F(p,null,u(f,n=>_(b(r),{key:n,variant:e,size:n},{default:l(()=>[t(m(n),1)]),_:2},1032,[`variant`,`size`])),64)),_(b(r),{variant:e,disabled:``},{default:l(()=>[...x[0]||=[t(`disabled`,-1)]]),_:1},8,[`variant`])])),64)),c(`div`,Oe,[x[2]||=c(`span`,{class:`w-24 text-muted-foreground`},`icon`,-1),(o(),F(p,null,u(h,t=>_(b(r),{key:t,variant:`outline`,size:t,"aria-label":t},{default:l(()=>[_(b(e))]),_:1},8,[`size`,`aria-label`])),64)),_(b(r),{variant:`outline`,size:`sm`},{default:l(()=>[_(b(e),{"data-icon":`inline-start`}),x[1]||=t(`with icon`,-1)]),_:1})])]),_:1}),_(B,{title:`Badge`},{default:l(()=>[c(`div`,ke,[(o(),F(p,null,u(g,e=>_(b(n),{key:e,variant:e},{default:l(()=>[t(m(e),1)]),_:2},1032,[`variant`])),64))])]),_:1}),_(B,{title:`Card`},{default:l(()=>[_(b(ne),{class:`max-w-sm`},{default:l(()=>[_(b(_e),null,{default:l(()=>[_(b(ve),null,{default:l(()=>[...x[3]||=[t(`Card title`,-1)]]),_:1}),_(b(ge),null,{default:l(()=>[...x[4]||=[t(`Card description, muted.`,-1)]]),_:1})]),_:1}),_(b(R),null,{default:l(()=>[...x[5]||=[t(`Card content.`,-1)]]),_:1})]),_:1})]),_:1}),_(B,{title:`Input and Label`,note:`empty, filled, invalid, disabled`},{default:l(()=>[c(`div`,Ae,[_(b(ue),null,{default:l(()=>[...x[6]||=[t(`Project path`,-1)]]),_:1}),_(b(D),{placeholder:`/path/to/project`}),_(b(D),{"model-value":`/Users/me/project`}),_(b(D),{"model-value":`/not/a/project`,"aria-invalid":`true`}),_(b(D),{"model-value":`disabled`,disabled:``})])]),_:1}),_(B,{title:`Separator`},{default:l(()=>[_(b(z)),c(`div`,$,[x[7]||=t(`left`,-1),_(b(z),{orientation:`vertical`}),x[8]||=t(`right`,-1)])]),_:1}),_(B,{title:`ToggleGroup`,note:`the Board / List switch`},{default:l(()=>[_(b(de),{type:`single`,variant:`outline`,size:`sm`,"model-value":`board`,"aria-label":`View`},{default:l(()=>[_(b(j),{value:`board`},{default:l(()=>[...x[9]||=[t(`Board`,-1)]]),_:1}),_(b(j),{value:`list`},{default:l(()=>[...x[10]||=[t(`List`,-1)]]),_:1})]),_:1})]),_:1}),_(B,{title:`Collapsible`,note:`open and folded`},{default:l(()=>[(o(),F(p,null,u([!0,!1],e=>_(b(E),{key:String(e),"default-open":e},{default:l(()=>[_(b(le),{class:`w-full border-b bg-muted/50 px-3 py-1.5 text-left`},{default:l(()=>[t(`Trigger (`+m(e?`open`:`folded`)+`)`,1)]),_:2},1024),_(b(ce),{class:`px-3 py-2`},{default:l(()=>[...x[11]||=[t(`Content`,-1)]]),_:1})]),_:2},1032,[`default-open`])),64))]),_:1}),x[14]||=c(`h2`,{class:`border-b px-4 py-2 font-bold tracking-widest uppercase`},`components`,-1),_(B,{title:`ProjectTabs`,note:`on the workspace; on a project; empty`},{default:l(()=>[_(O,{projects:b(Z),version:`0.1.0`},{default:l(()=>[_(i)]),_:1},8,[`projects`]),_(O,{projects:b(Z),current:`jig-ui`,version:`0.1.0`},null,8,[`projects`]),_(O,{projects:[]})]),_:1}),_(B,{title:`ThemeToggle`,note:`switches the whole page; the icon is the theme it switches to`},{default:l(()=>[c(`div`,null,[_(i)])]),_:1}),_(B,{title:`ProjectCard`,note:`ready with attention and stale housekeeping; nothing needs you; each state it cannot be read in`},{default:l(()=>[(o(!0),F(p,null,u(b(Z),e=>(o(),d(fe,{key:e.id,project:e},null,8,[`project`]))),128))]),_:1}),_(B,{title:`ConnectForm`,note:`idle, busy, refused`},{default:l(()=>[_(A,{busy:!1}),_(A,{busy:!0,"model-value":`/Users/me/project`}),_(A,{busy:!1,"model-value":`/Users/me/project/.claude/worktrees/x`,refusal:`That is a worktree. Connect the main checkout instead.`})]),_:1}),_(B,{title:`AttentionItem`,note:`no link; a pull request; another link`},{default:l(()=>[(o(!0),F(p,null,u(b(K),(e,t)=>(o(),d(ie,{key:t,item:e},null,8,[`item`]))),128))]),_:1}),_(B,{title:`ColumnHeader`,note:`each column, and one this page does not know`},{default:l(()=>[(o(!0),F(p,null,u(b(we),e=>(o(),d(re,{id:e.id,key:e.id,title:e.title,count:e.count},null,8,[`id`,`title`,`count`]))),128))]),_:1}),_(B,{title:`TaskCard`,note:`every note a card carries; open with details, while reading, unreadable`},{default:l(()=>[c(`div`,je,[(o(!0),F(p,null,u(b(U),e=>(o(),d(S,{key:e.id,project:`jig-ui`,task:e,open:!1},null,8,[`task`]))),128)),_(S,{project:`jig-ui`,task:b(U)[2],open:!0,details:b(Y)[0]},null,8,[`task`,`details`]),_(S,{project:`jig-ui`,task:b(U)[0],open:!0},null,8,[`task`]),_(S,{project:`jig-ui`,task:b(U)[1],open:!0,details:`unavailable`},null,8,[`task`])])]),_:1}),_(B,{title:`TaskRow`,note:`each note; selected`},{default:l(()=>[c(`div`,null,[(o(!0),F(p,null,u(b(U),(e,t)=>(o(),d(oe,{key:e.id,task:e,selected:t===2},null,8,[`task`,`selected`]))),128))])]),_:1}),_(B,{title:`BoardColumns`,note:`cards in every column, one open with its details; no live task`},{default:l(()=>[c(`div`,Me,[_(T,{project:`jig-ui`,columns:b(W),selected:b(U)[2].id,details:b(Y)[0]},null,8,[`columns`,`selected`,`details`])]),c(`div`,Ne,[_(T,{project:`jig-ui`,columns:b(G)},null,8,[`columns`])])]),_:1}),_(B,{title:`BoardList`,note:`a row picked with its details; nothing picked; no live task`},{default:l(()=>[c(`div`,Pe,[_(w,{project:`jig-ui`,columns:b(W),selected:b(U)[2].id,details:b(Y)[0]},null,8,[`columns`,`selected`,`details`])]),c(`div`,Fe,[_(w,{project:`jig-ui`,columns:b(W)},null,8,[`columns`])]),c(`div`,Ie,[_(w,{project:`jig-ui`,columns:b(G)},null,8,[`columns`])])]),_:1}),_(B,{title:`TaskFacts`,note:`everything a task can carry; a task not started`},{default:l(()=>[(o(!0),F(p,null,u(b(Y),e=>(o(),d(P,{key:e.id,details:e,class:`border-b pb-3`},null,8,[`details`]))),128))]),_:1}),_(B,{title:`TaskBrief`,note:`facts, actions and the link to the full page`},{default:l(()=>[_(me,{project:`jig-ui`,task:b(Y)[1].id,details:b(Y)[1]},null,8,[`task`,`details`])]),_:1}),_(B,{title:`ActionList`,note:`Jig's offers, one unknown id; none`},{default:l(()=>[_(C,{actions:b(q)},null,8,[`actions`]),_(C,{actions:[]})]),_:1}),_(B,{title:`CopyCommand`,note:`short; long enough to be cut`},{default:l(()=>[_(N,{command:`jig task pause design-system`}),_(N,{command:`jig task autopilot a-task-with-a-rather-long-identifier-that-wraps resume --answer 'a long answer to the question'`,class:`max-w-xs`})]),_:1}),_(B,{title:`ProgressBar`,note:`none, part, all, an empty roadmap`},{default:l(()=>[(o(),F(p,null,u([[0,4],[3,9],[5,5],[0,0]],([e,t])=>_(pe,{key:`${e}/${t}`,done:e,total:t},null,8,[`done`,`total`])),64))]),_:1}),_(B,{title:`SpecItemRow`,note:`done with a live task; open with dependencies; a task no longer live; no task; fog`},{default:l(()=>[c(`ul`,null,[(o(!0),F(p,null,u(b(Q),(e,t)=>(o(),d(se,{key:t,item:e,project:`jig-ui`,live:t<2},null,8,[`item`,`live`]))),128))])]),_:1}),_(B,{title:`SpecCard`,note:`no epic, a finished phase folded; an open epic as of the last fetch; a lost epic branch; missing files`},{default:l(()=>[(o(!0),F(p,null,u(b(Te),e=>(o(),d(ae,{key:e.id,spec:e,project:`jig-ui`,live:[`design-system`,`specs-and-epics`]},null,8,[`spec`]))),128))]),_:1})]))}});export{Le as default};
|