@bamptee/aia-code 1.0.0 → 1.0.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/package.json +1 -1
- package/src/commands/quick.js +8 -46
- package/src/providers/anthropic.js +2 -2
- package/src/providers/cli-runner.js +3 -1
- package/src/providers/gemini.js +2 -2
- package/src/providers/openai.js +2 -2
- package/src/services/model-call.js +2 -2
- package/src/services/quick.js +40 -0
- package/src/services/runner.js +2 -2
- package/src/ui/api/features.js +75 -4
- package/src/ui/public/components/dashboard.js +95 -3
- package/src/ui/public/components/feature-detail.js +95 -10
- package/src/ui/public/main.js +47 -0
package/package.json
CHANGED
package/src/commands/quick.js
CHANGED
|
@@ -1,58 +1,20 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import yaml from 'yaml';
|
|
5
|
-
import { AIA_DIR, FEATURE_STEPS, QUICK_STEPS, STEP_STATUS } from '../constants.js';
|
|
6
|
-
import { createFeature, validateFeatureName } from '../services/feature.js';
|
|
7
|
-
import { runStep } from '../services/runner.js';
|
|
8
|
-
|
|
9
|
-
async function skipEarlySteps(feature, root) {
|
|
10
|
-
const statusFile = path.join(root, AIA_DIR, 'features', feature, 'status.yaml');
|
|
11
|
-
const raw = await fs.readFile(statusFile, 'utf-8');
|
|
12
|
-
const status = yaml.parse(raw);
|
|
13
|
-
|
|
14
|
-
for (const step of FEATURE_STEPS) {
|
|
15
|
-
if (!QUICK_STEPS.includes(step)) {
|
|
16
|
-
status.steps[step] = STEP_STATUS.DONE;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
status.current_step = QUICK_STEPS[0];
|
|
20
|
-
|
|
21
|
-
await fs.writeFile(statusFile, yaml.stringify(status), 'utf-8');
|
|
22
|
-
}
|
|
2
|
+
import { runQuick } from '../services/quick.js';
|
|
3
|
+
import { QUICK_STEPS } from '../constants.js';
|
|
23
4
|
|
|
24
5
|
export function registerQuickCommand(program) {
|
|
25
6
|
program
|
|
26
7
|
.command('quick <name> [description]')
|
|
27
|
-
.description(
|
|
8
|
+
.description(`Quick story/ticket: create feature then run ${QUICK_STEPS.join(' \u2192 ')}`)
|
|
28
9
|
.option('-v, --verbose', 'Show AI thinking/tool usage')
|
|
29
10
|
.option('-a, --apply', 'Force agent mode (file editing) for all steps')
|
|
30
11
|
.action(async (name, description, opts) => {
|
|
31
12
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!(await fs.pathExists(featureDir))) {
|
|
38
|
-
await createFeature(name, root);
|
|
39
|
-
console.log(chalk.green(`Feature "${name}" created.`));
|
|
40
|
-
const initPath = path.join(AIA_DIR, 'features', name, 'init.md');
|
|
41
|
-
console.log(chalk.gray(`Using ${initPath} as input.`));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
await skipEarlySteps(name, root);
|
|
45
|
-
|
|
46
|
-
for (const step of QUICK_STEPS) {
|
|
47
|
-
console.log(chalk.cyan(`\n--- Running ${step} ---\n`));
|
|
48
|
-
await runStep(step, name, {
|
|
49
|
-
description,
|
|
50
|
-
verbose: opts.verbose,
|
|
51
|
-
apply: opts.apply,
|
|
52
|
-
root,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
13
|
+
await runQuick(name, {
|
|
14
|
+
description,
|
|
15
|
+
verbose: opts.verbose,
|
|
16
|
+
apply: opts.apply,
|
|
17
|
+
});
|
|
56
18
|
console.log(chalk.green(`\nQuick pipeline completed for "${name}".`));
|
|
57
19
|
} catch (err) {
|
|
58
20
|
console.error(chalk.red(err.message));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runCli } from './cli-runner.js';
|
|
2
2
|
|
|
3
|
-
export async function generate(prompt, model, { verbose = false, apply = false } = {}) {
|
|
3
|
+
export async function generate(prompt, model, { verbose = false, apply = false, onData } = {}) {
|
|
4
4
|
const args = ['-p'];
|
|
5
5
|
if (model) {
|
|
6
6
|
args.push('--model', model);
|
|
@@ -13,5 +13,5 @@ export async function generate(prompt, model, { verbose = false, apply = false }
|
|
|
13
13
|
}
|
|
14
14
|
args.push('-');
|
|
15
15
|
|
|
16
|
-
return runCli('claude', args, { stdin: prompt, verbose: verbose || apply, apply });
|
|
16
|
+
return runCli('claude', args, { stdin: prompt, verbose: verbose || apply, apply, onData });
|
|
17
17
|
}
|
|
@@ -4,7 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
const DEFAULT_IDLE_TIMEOUT_MS = 180_000;
|
|
5
5
|
const AGENT_IDLE_TIMEOUT_MS = 600_000;
|
|
6
6
|
|
|
7
|
-
export function runCli(command, args, { stdin: stdinData, verbose = false, apply = false, idleTimeoutMs } = {}) {
|
|
7
|
+
export function runCli(command, args, { stdin: stdinData, verbose = false, apply = false, idleTimeoutMs, onData } = {}) {
|
|
8
8
|
if (!idleTimeoutMs) {
|
|
9
9
|
idleTimeoutMs = apply ? AGENT_IDLE_TIMEOUT_MS : DEFAULT_IDLE_TIMEOUT_MS;
|
|
10
10
|
}
|
|
@@ -41,6 +41,7 @@ export function runCli(command, args, { stdin: stdinData, verbose = false, apply
|
|
|
41
41
|
const text = data.toString();
|
|
42
42
|
process.stdout.write(text);
|
|
43
43
|
chunks.push(text);
|
|
44
|
+
if (onData) onData({ type: 'stdout', text });
|
|
44
45
|
resetTimer();
|
|
45
46
|
});
|
|
46
47
|
|
|
@@ -50,6 +51,7 @@ export function runCli(command, args, { stdin: stdinData, verbose = false, apply
|
|
|
50
51
|
if (verbose) {
|
|
51
52
|
process.stderr.write(chalk.gray(text));
|
|
52
53
|
}
|
|
54
|
+
if (onData) onData({ type: 'stderr', text });
|
|
53
55
|
resetTimer();
|
|
54
56
|
});
|
|
55
57
|
|
package/src/providers/gemini.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runCli } from './cli-runner.js';
|
|
2
2
|
|
|
3
|
-
export async function generate(prompt, model, { verbose = false, apply = false } = {}) {
|
|
3
|
+
export async function generate(prompt, model, { verbose = false, apply = false, onData } = {}) {
|
|
4
4
|
const args = [];
|
|
5
5
|
if (model) {
|
|
6
6
|
args.push('-m', model);
|
|
@@ -10,5 +10,5 @@ export async function generate(prompt, model, { verbose = false, apply = false }
|
|
|
10
10
|
}
|
|
11
11
|
args.push('-');
|
|
12
12
|
|
|
13
|
-
return runCli('gemini', args, { stdin: prompt, verbose: verbose || apply, apply });
|
|
13
|
+
return runCli('gemini', args, { stdin: prompt, verbose: verbose || apply, apply, onData });
|
|
14
14
|
}
|
package/src/providers/openai.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runCli } from './cli-runner.js';
|
|
2
2
|
|
|
3
|
-
export async function generate(prompt, model, { verbose = false, apply = false } = {}) {
|
|
3
|
+
export async function generate(prompt, model, { verbose = false, apply = false, onData } = {}) {
|
|
4
4
|
const args = ['exec'];
|
|
5
5
|
if (model) {
|
|
6
6
|
args.push('-c', `model="${model}"`);
|
|
@@ -10,5 +10,5 @@ export async function generate(prompt, model, { verbose = false, apply = false }
|
|
|
10
10
|
}
|
|
11
11
|
args.push('-');
|
|
12
12
|
|
|
13
|
-
return runCli('codex', args, { stdin: prompt, verbose: verbose || apply, apply });
|
|
13
|
+
return runCli('codex', args, { stdin: prompt, verbose: verbose || apply, apply, onData });
|
|
14
14
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { resolveModelAlias } from '../providers/registry.js';
|
|
3
3
|
|
|
4
|
-
export async function callModel(model, prompt, { verbose = false, apply = false } = {}) {
|
|
4
|
+
export async function callModel(model, prompt, { verbose = false, apply = false, onData } = {}) {
|
|
5
5
|
const resolved = resolveModelAlias(model);
|
|
6
6
|
const displayName = resolved.model ?? `${model} (CLI default)`;
|
|
7
7
|
const mode = apply ? 'agent' : 'print';
|
|
8
8
|
|
|
9
9
|
console.log(chalk.yellow(`[AI] Calling ${displayName} (${mode} mode)...`));
|
|
10
10
|
|
|
11
|
-
return resolved.provider.generate(prompt, resolved.model, { verbose, apply });
|
|
11
|
+
return resolved.provider.generate(prompt, resolved.model, { verbose, apply, onData });
|
|
12
12
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
import { AIA_DIR, FEATURE_STEPS, QUICK_STEPS, STEP_STATUS } from '../constants.js';
|
|
5
|
+
import { createFeature, validateFeatureName } from './feature.js';
|
|
6
|
+
import { runStep } from './runner.js';
|
|
7
|
+
|
|
8
|
+
export async function skipEarlySteps(feature, root) {
|
|
9
|
+
const statusFile = path.join(root, AIA_DIR, 'features', feature, 'status.yaml');
|
|
10
|
+
const raw = await fs.readFile(statusFile, 'utf-8');
|
|
11
|
+
const status = yaml.parse(raw);
|
|
12
|
+
|
|
13
|
+
for (const step of FEATURE_STEPS) {
|
|
14
|
+
if (!QUICK_STEPS.includes(step)) {
|
|
15
|
+
status.steps[step] = STEP_STATUS.DONE;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
status.current_step = QUICK_STEPS[0];
|
|
19
|
+
|
|
20
|
+
await fs.writeFile(statusFile, yaml.stringify(status), 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runQuick(name, { description, verbose = false, apply = false, root = process.cwd(), onData } = {}) {
|
|
24
|
+
validateFeatureName(name);
|
|
25
|
+
|
|
26
|
+
const featureDir = path.join(root, AIA_DIR, 'features', name);
|
|
27
|
+
const created = !(await fs.pathExists(featureDir));
|
|
28
|
+
|
|
29
|
+
if (created) {
|
|
30
|
+
await createFeature(name, root);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await skipEarlySteps(name, root);
|
|
34
|
+
|
|
35
|
+
for (const step of QUICK_STEPS) {
|
|
36
|
+
await runStep(step, name, { description, verbose, apply, root, onData });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { created };
|
|
40
|
+
}
|
package/src/services/runner.js
CHANGED
|
@@ -8,7 +8,7 @@ import { callModel } from './model-call.js';
|
|
|
8
8
|
import { loadStatus, updateStepStatus } from './status.js';
|
|
9
9
|
import { logExecution } from '../logger.js';
|
|
10
10
|
|
|
11
|
-
export async function runStep(step, feature, { description, verbose = false, apply = false, root = process.cwd() } = {}) {
|
|
11
|
+
export async function runStep(step, feature, { description, verbose = false, apply = false, root = process.cwd(), onData } = {}) {
|
|
12
12
|
if (!FEATURE_STEPS.includes(step)) {
|
|
13
13
|
throw new Error(`Unknown step "${step}". Valid steps: ${FEATURE_STEPS.join(', ')}`);
|
|
14
14
|
}
|
|
@@ -30,7 +30,7 @@ export async function runStep(step, feature, { description, verbose = false, app
|
|
|
30
30
|
const prompt = await buildPrompt(feature, step, { description, root });
|
|
31
31
|
|
|
32
32
|
const start = performance.now();
|
|
33
|
-
const output = await callModel(model, prompt, { verbose, apply: shouldApply });
|
|
33
|
+
const output = await callModel(model, prompt, { verbose, apply: shouldApply, onData });
|
|
34
34
|
const duration = performance.now() - start;
|
|
35
35
|
|
|
36
36
|
const outputPath = path.join(root, AIA_DIR, 'features', feature, `${step}.md`);
|
package/src/ui/api/features.js
CHANGED
|
@@ -4,8 +4,22 @@ import { AIA_DIR } from '../../constants.js';
|
|
|
4
4
|
import { loadStatus, updateStepStatus, resetStep } from '../../services/status.js';
|
|
5
5
|
import { createFeature, validateFeatureName } from '../../services/feature.js';
|
|
6
6
|
import { runStep } from '../../services/runner.js';
|
|
7
|
+
import { runQuick } from '../../services/quick.js';
|
|
7
8
|
import { json, error } from '../router.js';
|
|
8
9
|
|
|
10
|
+
function sseHeaders(res) {
|
|
11
|
+
res.writeHead(200, {
|
|
12
|
+
'Content-Type': 'text/event-stream',
|
|
13
|
+
'Cache-Control': 'no-cache',
|
|
14
|
+
'Connection': 'keep-alive',
|
|
15
|
+
'Access-Control-Allow-Origin': '*',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sseSend(res, event, data) {
|
|
20
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
export function registerFeatureRoutes(router) {
|
|
10
24
|
// List all features
|
|
11
25
|
router.get('/api/features', async (req, res, { root }) => {
|
|
@@ -70,20 +84,77 @@ export function registerFeatureRoutes(router) {
|
|
|
70
84
|
}
|
|
71
85
|
});
|
|
72
86
|
|
|
73
|
-
// Run a step
|
|
87
|
+
// Run a step with SSE streaming
|
|
74
88
|
router.post('/api/features/:name/run/:step', async (req, res, { params, root, parseBody }) => {
|
|
75
89
|
const body = await parseBody();
|
|
90
|
+
sseHeaders(res);
|
|
91
|
+
sseSend(res, 'status', { step: params.step, status: 'started' });
|
|
92
|
+
|
|
93
|
+
const onData = ({ type, text }) => {
|
|
94
|
+
try { sseSend(res, 'log', { type, text }); } catch {}
|
|
95
|
+
};
|
|
96
|
+
|
|
76
97
|
try {
|
|
77
98
|
const output = await runStep(params.step, params.name, {
|
|
78
99
|
description: body.description,
|
|
79
|
-
verbose:
|
|
100
|
+
verbose: true,
|
|
80
101
|
apply: body.apply || false,
|
|
81
102
|
root,
|
|
103
|
+
onData,
|
|
82
104
|
});
|
|
83
|
-
|
|
105
|
+
sseSend(res, 'done', { step: params.step, output: output.slice(0, 500) });
|
|
84
106
|
} catch (err) {
|
|
85
|
-
|
|
107
|
+
sseSend(res, 'error', { message: err.message });
|
|
108
|
+
}
|
|
109
|
+
res.end();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Quick ticket with SSE streaming (dev-plan -> implement -> review)
|
|
113
|
+
router.post('/api/features/:name/quick', async (req, res, { params, root, parseBody }) => {
|
|
114
|
+
const body = await parseBody();
|
|
115
|
+
sseHeaders(res);
|
|
116
|
+
sseSend(res, 'status', { status: 'started', mode: 'quick' });
|
|
117
|
+
|
|
118
|
+
const onData = ({ type, text }) => {
|
|
119
|
+
try { sseSend(res, 'log', { type, text }); } catch {}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await runQuick(params.name, {
|
|
124
|
+
description: body.description,
|
|
125
|
+
apply: body.apply || false,
|
|
126
|
+
root,
|
|
127
|
+
onData,
|
|
128
|
+
});
|
|
129
|
+
sseSend(res, 'done', { status: 'completed' });
|
|
130
|
+
} catch (err) {
|
|
131
|
+
sseSend(res, 'error', { message: err.message });
|
|
132
|
+
}
|
|
133
|
+
res.end();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Quick ticket with SSE streaming (create + run)
|
|
137
|
+
router.post('/api/quick', async (req, res, { root, parseBody }) => {
|
|
138
|
+
const body = await parseBody();
|
|
139
|
+
sseHeaders(res);
|
|
140
|
+
sseSend(res, 'status', { status: 'started', mode: 'quick', name: body.name });
|
|
141
|
+
|
|
142
|
+
const onData = ({ type, text }) => {
|
|
143
|
+
try { sseSend(res, 'log', { type, text }); } catch {}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await runQuick(body.name, {
|
|
148
|
+
description: body.description,
|
|
149
|
+
apply: body.apply || false,
|
|
150
|
+
root,
|
|
151
|
+
onData,
|
|
152
|
+
});
|
|
153
|
+
sseSend(res, 'done', { status: 'completed', name: body.name });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
sseSend(res, 'error', { message: err.message });
|
|
86
156
|
}
|
|
157
|
+
res.end();
|
|
87
158
|
});
|
|
88
159
|
|
|
89
160
|
// Reset a step
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { api } from '/main.js';
|
|
2
|
+
import { api, streamPost } from '/main.js';
|
|
3
3
|
|
|
4
4
|
const STATUS_CLASSES = {
|
|
5
5
|
done: 'step-done',
|
|
@@ -79,6 +79,94 @@ function NewFeatureForm({ onCreated }) {
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function LogViewer({ logs }) {
|
|
83
|
+
const ref = React.useRef(null);
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
if (ref.current) ref.current.scrollTop = ref.current.scrollHeight;
|
|
86
|
+
}, [logs]);
|
|
87
|
+
|
|
88
|
+
if (!logs.length) return null;
|
|
89
|
+
return React.createElement('pre', {
|
|
90
|
+
ref,
|
|
91
|
+
className: 'bg-black/50 border border-aia-border rounded p-3 text-xs text-slate-400 overflow-auto max-h-48 whitespace-pre-wrap',
|
|
92
|
+
}, logs.join(''));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function QuickTicketForm({ onDone }) {
|
|
96
|
+
const [name, setName] = React.useState('');
|
|
97
|
+
const [description, setDescription] = React.useState('');
|
|
98
|
+
const [apply, setApply] = React.useState(false);
|
|
99
|
+
const [running, setRunning] = React.useState(false);
|
|
100
|
+
const [err, setErr] = React.useState('');
|
|
101
|
+
const [logs, setLogs] = React.useState([]);
|
|
102
|
+
|
|
103
|
+
async function handleSubmit(e) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
setErr('');
|
|
106
|
+
setRunning(true);
|
|
107
|
+
setLogs([]);
|
|
108
|
+
|
|
109
|
+
const res = await streamPost('/quick', { name, description, apply }, {
|
|
110
|
+
onLog: (text) => setLogs(prev => [...prev, text]),
|
|
111
|
+
onStatus: (data) => setLogs(prev => [...prev, `[${data.status}] ${data.name || ''}\n`]),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (res.ok) {
|
|
115
|
+
setName('');
|
|
116
|
+
setDescription('');
|
|
117
|
+
setLogs([]);
|
|
118
|
+
onDone();
|
|
119
|
+
} else {
|
|
120
|
+
setErr(res.error);
|
|
121
|
+
}
|
|
122
|
+
setRunning(false);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return React.createElement('form', {
|
|
126
|
+
onSubmit: handleSubmit,
|
|
127
|
+
className: 'bg-aia-card border border-amber-500/30 rounded-lg p-4 space-y-3',
|
|
128
|
+
},
|
|
129
|
+
React.createElement('h3', { className: 'text-sm font-semibold text-amber-400' }, 'Quick Ticket'),
|
|
130
|
+
React.createElement('p', { className: 'text-xs text-slate-500' }, 'dev-plan \u2192 implement \u2192 review'),
|
|
131
|
+
React.createElement('div', { className: 'flex gap-2' },
|
|
132
|
+
React.createElement('input', {
|
|
133
|
+
type: 'text',
|
|
134
|
+
value: name,
|
|
135
|
+
onChange: e => setName(e.target.value),
|
|
136
|
+
placeholder: 'ticket-name',
|
|
137
|
+
disabled: running,
|
|
138
|
+
className: 'bg-slate-900 border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 focus:border-amber-400 focus:outline-none flex-shrink-0',
|
|
139
|
+
}),
|
|
140
|
+
React.createElement('input', {
|
|
141
|
+
type: 'text',
|
|
142
|
+
value: description,
|
|
143
|
+
onChange: e => setDescription(e.target.value),
|
|
144
|
+
placeholder: 'Short description (optional)',
|
|
145
|
+
disabled: running,
|
|
146
|
+
className: 'bg-slate-900 border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 focus:border-amber-400 focus:outline-none flex-1',
|
|
147
|
+
}),
|
|
148
|
+
),
|
|
149
|
+
React.createElement('div', { className: 'flex items-center gap-4' },
|
|
150
|
+
React.createElement('label', { className: 'flex items-center gap-2 text-xs text-slate-400 cursor-pointer' },
|
|
151
|
+
React.createElement('input', {
|
|
152
|
+
type: 'checkbox',
|
|
153
|
+
checked: apply,
|
|
154
|
+
onChange: e => setApply(e.target.checked),
|
|
155
|
+
disabled: running,
|
|
156
|
+
}),
|
|
157
|
+
'Agent mode (--apply)'
|
|
158
|
+
),
|
|
159
|
+
React.createElement('button', {
|
|
160
|
+
type: 'submit',
|
|
161
|
+
disabled: running || !name,
|
|
162
|
+
className: 'bg-amber-500/20 text-amber-400 border border-amber-500/30 rounded px-4 py-1.5 text-sm hover:bg-amber-500/30 disabled:opacity-40',
|
|
163
|
+
}, running ? 'Running...' : 'Run Quick Ticket'),
|
|
164
|
+
),
|
|
165
|
+
React.createElement(LogViewer, { logs }),
|
|
166
|
+
err && React.createElement('p', { className: 'text-red-400 text-xs' }, err),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
82
170
|
export function Dashboard() {
|
|
83
171
|
const [features, setFeatures] = React.useState([]);
|
|
84
172
|
const [loading, setLoading] = React.useState(true);
|
|
@@ -93,11 +181,15 @@ export function Dashboard() {
|
|
|
93
181
|
|
|
94
182
|
React.useEffect(() => { load(); }, []);
|
|
95
183
|
|
|
96
|
-
return React.createElement('div',
|
|
97
|
-
React.createElement('div', { className: 'flex items-center justify-between
|
|
184
|
+
return React.createElement('div', { className: 'space-y-6' },
|
|
185
|
+
React.createElement('div', { className: 'flex items-center justify-between' },
|
|
98
186
|
React.createElement('h1', { className: 'text-xl font-bold text-slate-100' }, 'Features'),
|
|
99
187
|
React.createElement(NewFeatureForm, { onCreated: load }),
|
|
100
188
|
),
|
|
189
|
+
|
|
190
|
+
// Quick ticket
|
|
191
|
+
React.createElement(QuickTicketForm, { onDone: load }),
|
|
192
|
+
|
|
101
193
|
loading
|
|
102
194
|
? React.createElement('p', { className: 'text-slate-500' }, 'Loading...')
|
|
103
195
|
: features.length === 0
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { api } from '/main.js';
|
|
2
|
+
import { api, streamPost } from '/main.js';
|
|
3
3
|
|
|
4
4
|
const STATUS_CLASSES = {
|
|
5
5
|
done: 'step-done',
|
|
@@ -65,23 +65,103 @@ function FileEditor({ name, filename, onSaved }) {
|
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function QuickRunButton({ name, onDone }) {
|
|
69
|
+
const [running, setRunning] = React.useState(false);
|
|
70
|
+
const [description, setDescription] = React.useState('');
|
|
71
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
72
|
+
const [err, setErr] = React.useState(null);
|
|
73
|
+
const [logs, setLogs] = React.useState([]);
|
|
74
|
+
|
|
75
|
+
async function run() {
|
|
76
|
+
setRunning(true);
|
|
77
|
+
setErr(null);
|
|
78
|
+
setLogs([]);
|
|
79
|
+
|
|
80
|
+
const res = await streamPost(`/features/${name}/quick`, { description }, {
|
|
81
|
+
onLog: (text) => setLogs(prev => [...prev, text]),
|
|
82
|
+
onStatus: (data) => setLogs(prev => [...prev, `[${data.status}] ${data.mode || ''}\n`]),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (res.ok) {
|
|
86
|
+
if (onDone) onDone();
|
|
87
|
+
} else {
|
|
88
|
+
setErr(res.error);
|
|
89
|
+
}
|
|
90
|
+
setRunning(false);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!expanded) {
|
|
94
|
+
return React.createElement('button', {
|
|
95
|
+
onClick: () => setExpanded(true),
|
|
96
|
+
className: 'bg-amber-500/20 text-amber-400 border border-amber-500/30 rounded px-3 py-1.5 text-xs hover:bg-amber-500/30',
|
|
97
|
+
}, 'Quick Ticket (dev-plan \u2192 implement \u2192 review)');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return React.createElement('div', { className: 'bg-slate-900 border border-amber-500/30 rounded p-4 space-y-3' },
|
|
101
|
+
React.createElement('h4', { className: 'text-sm font-semibold text-amber-400' }, 'Quick Ticket'),
|
|
102
|
+
React.createElement('p', { className: 'text-xs text-slate-500' }, 'Skips early steps, runs dev-plan \u2192 implement \u2192 review'),
|
|
103
|
+
React.createElement('input', {
|
|
104
|
+
type: 'text',
|
|
105
|
+
value: description,
|
|
106
|
+
onChange: e => setDescription(e.target.value),
|
|
107
|
+
placeholder: 'Optional description...',
|
|
108
|
+
disabled: running,
|
|
109
|
+
className: 'w-full bg-aia-card border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 focus:border-amber-400 focus:outline-none',
|
|
110
|
+
}),
|
|
111
|
+
React.createElement('div', { className: 'flex gap-2' },
|
|
112
|
+
React.createElement('button', {
|
|
113
|
+
onClick: run,
|
|
114
|
+
disabled: running,
|
|
115
|
+
className: 'bg-amber-500/20 text-amber-400 border border-amber-500/30 rounded px-4 py-1.5 text-sm hover:bg-amber-500/30 disabled:opacity-40',
|
|
116
|
+
}, running ? 'Running...' : 'Run Quick'),
|
|
117
|
+
React.createElement('button', {
|
|
118
|
+
onClick: () => setExpanded(false),
|
|
119
|
+
disabled: running,
|
|
120
|
+
className: 'text-slate-500 hover:text-slate-300 text-xs',
|
|
121
|
+
}, 'Cancel'),
|
|
122
|
+
),
|
|
123
|
+
React.createElement(LogViewer, { logs }),
|
|
124
|
+
err && React.createElement('p', { className: 'text-red-400 text-xs' }, err),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function LogViewer({ logs }) {
|
|
129
|
+
const ref = React.useRef(null);
|
|
130
|
+
React.useEffect(() => {
|
|
131
|
+
if (ref.current) ref.current.scrollTop = ref.current.scrollHeight;
|
|
132
|
+
}, [logs]);
|
|
133
|
+
|
|
134
|
+
if (!logs.length) return null;
|
|
135
|
+
return React.createElement('pre', {
|
|
136
|
+
ref,
|
|
137
|
+
className: 'bg-black/50 border border-aia-border rounded p-3 text-xs text-slate-400 overflow-auto max-h-64 whitespace-pre-wrap',
|
|
138
|
+
}, logs.join(''));
|
|
139
|
+
}
|
|
140
|
+
|
|
68
141
|
function RunPanel({ name, step, onDone }) {
|
|
69
142
|
const [description, setDescription] = React.useState('');
|
|
70
143
|
const [apply, setApply] = React.useState(false);
|
|
71
144
|
const [running, setRunning] = React.useState(false);
|
|
72
145
|
const [result, setResult] = React.useState(null);
|
|
73
|
-
const [
|
|
146
|
+
const [err, setErr] = React.useState(null);
|
|
147
|
+
const [logs, setLogs] = React.useState([]);
|
|
74
148
|
|
|
75
149
|
async function run() {
|
|
76
150
|
setRunning(true);
|
|
77
151
|
setResult(null);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
152
|
+
setErr(null);
|
|
153
|
+
setLogs([]);
|
|
154
|
+
|
|
155
|
+
const res = await streamPost(`/features/${name}/run/${step}`, { description, apply }, {
|
|
156
|
+
onLog: (text) => setLogs(prev => [...prev, text]),
|
|
157
|
+
onStatus: (data) => setLogs(prev => [...prev, `[${data.status}] ${data.step || ''}\n`]),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (res.ok) {
|
|
81
161
|
setResult('Step completed.');
|
|
82
162
|
if (onDone) onDone();
|
|
83
|
-
}
|
|
84
|
-
|
|
163
|
+
} else {
|
|
164
|
+
setErr(res.error);
|
|
85
165
|
}
|
|
86
166
|
setRunning(false);
|
|
87
167
|
}
|
|
@@ -91,7 +171,7 @@ function RunPanel({ name, step, onDone }) {
|
|
|
91
171
|
await api.post(`/features/${name}/reset/${step}`);
|
|
92
172
|
if (onDone) onDone();
|
|
93
173
|
} catch (e) {
|
|
94
|
-
|
|
174
|
+
setErr(e.message);
|
|
95
175
|
}
|
|
96
176
|
}
|
|
97
177
|
|
|
@@ -102,6 +182,7 @@ function RunPanel({ name, step, onDone }) {
|
|
|
102
182
|
value: description,
|
|
103
183
|
onChange: e => setDescription(e.target.value),
|
|
104
184
|
placeholder: 'Optional description...',
|
|
185
|
+
disabled: running,
|
|
105
186
|
className: 'w-full bg-aia-card border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 focus:border-aia-accent focus:outline-none',
|
|
106
187
|
}),
|
|
107
188
|
React.createElement('div', { className: 'flex items-center gap-4' },
|
|
@@ -110,6 +191,7 @@ function RunPanel({ name, step, onDone }) {
|
|
|
110
191
|
type: 'checkbox',
|
|
111
192
|
checked: apply,
|
|
112
193
|
onChange: e => setApply(e.target.checked),
|
|
194
|
+
disabled: running,
|
|
113
195
|
className: 'rounded',
|
|
114
196
|
}),
|
|
115
197
|
'Agent mode (--apply)'
|
|
@@ -125,9 +207,9 @@ function RunPanel({ name, step, onDone }) {
|
|
|
125
207
|
className: 'text-slate-500 hover:text-slate-300 text-xs',
|
|
126
208
|
}, 'Reset'),
|
|
127
209
|
),
|
|
128
|
-
|
|
210
|
+
React.createElement(LogViewer, { logs }),
|
|
129
211
|
result && React.createElement('p', { className: 'text-emerald-400 text-xs' }, result),
|
|
130
|
-
|
|
212
|
+
err && React.createElement('p', { className: 'text-red-400 text-xs' }, err),
|
|
131
213
|
);
|
|
132
214
|
}
|
|
133
215
|
|
|
@@ -160,6 +242,9 @@ export function FeatureDetail({ name }) {
|
|
|
160
242
|
feature.current_step && React.createElement('span', { className: 'text-xs bg-aia-accent/20 text-aia-accent px-2 py-0.5 rounded' }, feature.current_step),
|
|
161
243
|
),
|
|
162
244
|
|
|
245
|
+
// Quick run
|
|
246
|
+
React.createElement(QuickRunButton, { name, onDone: load }),
|
|
247
|
+
|
|
163
248
|
// Pipeline
|
|
164
249
|
React.createElement('div', { className: 'flex flex-wrap gap-2' },
|
|
165
250
|
...Object.entries(steps).map(([step, status]) =>
|
package/src/ui/public/main.js
CHANGED
|
@@ -31,6 +31,53 @@ export const api = {
|
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// --- SSE stream helper ---
|
|
35
|
+
// POST with SSE response. Calls onLog(text) for each log chunk, returns { ok, error }.
|
|
36
|
+
export async function streamPost(path, body, { onLog, onStatus }) {
|
|
37
|
+
const res = await fetch(`/api${path}`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const reader = res.body.getReader();
|
|
44
|
+
const decoder = new TextDecoder();
|
|
45
|
+
let buffer = '';
|
|
46
|
+
let result = { ok: true };
|
|
47
|
+
|
|
48
|
+
while (true) {
|
|
49
|
+
const { done, value } = await reader.read();
|
|
50
|
+
if (done) break;
|
|
51
|
+
buffer += decoder.decode(value, { stream: true });
|
|
52
|
+
|
|
53
|
+
const lines = buffer.split('\n');
|
|
54
|
+
buffer = lines.pop(); // keep incomplete line
|
|
55
|
+
|
|
56
|
+
let eventType = null;
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (line.startsWith('event: ')) {
|
|
59
|
+
eventType = line.slice(7);
|
|
60
|
+
} else if (line.startsWith('data: ') && eventType) {
|
|
61
|
+
try {
|
|
62
|
+
const data = JSON.parse(line.slice(6));
|
|
63
|
+
if (eventType === 'log' && onLog) {
|
|
64
|
+
onLog(data.text, data.type);
|
|
65
|
+
} else if (eventType === 'status' && onStatus) {
|
|
66
|
+
onStatus(data);
|
|
67
|
+
} else if (eventType === 'error') {
|
|
68
|
+
result = { ok: false, error: data.message };
|
|
69
|
+
} else if (eventType === 'done') {
|
|
70
|
+
result = { ok: true, ...data };
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
eventType = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
34
81
|
// --- Simple hash router ---
|
|
35
82
|
function useHashRoute() {
|
|
36
83
|
const [route, setRoute] = React.useState(window.location.hash || '#/');
|