@aiwg/cockpit 2026.7.7 → 2026.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -4
- package/bridge/src/server.mjs +372 -27
- package/bridge/src/smoke.mjs +10 -4
- package/contrib/aiwg-core.json +24 -2
- package/contrib/contribution.schema.json +28 -0
- package/package.json +1 -1
- package/shell-core/keychain.mjs +3 -0
- package/shell-core/runtime.mjs +3 -0
- package/web/src/App.test.tsx +146 -2
- package/web/src/App.tsx +9 -0
- package/web/src/components/Actions.tsx +80 -3
- package/web/src/components/Explore.tsx +89 -2
- package/web/src/components/Memory.tsx +145 -0
- package/web/src/components/Missions.tsx +135 -0
- package/web/src/components/Telemetry.tsx +85 -0
- package/web/src/styles.css +51 -1
- package/web/src/types.ts +67 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { api } from '../api';
|
|
3
|
+
import { fmtId } from '../util';
|
|
4
|
+
import type { MissionProjection, MissionsResponse } from '../types';
|
|
5
|
+
|
|
6
|
+
interface MemoryNote {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
body: string;
|
|
10
|
+
tags: string[];
|
|
11
|
+
created_at: string;
|
|
12
|
+
mission_id?: string;
|
|
13
|
+
source: 'operator' | 'mission';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const STORAGE_KEY = 'aiwg.cockpit.memory.notes.v1';
|
|
17
|
+
|
|
18
|
+
export function Memory({ refreshTick = 0 }: { refreshTick?: number }) {
|
|
19
|
+
const [notes, setNotes] = useState<MemoryNote[]>(() => readNotes());
|
|
20
|
+
const [missions, setMissions] = useState<MissionProjection[]>([]);
|
|
21
|
+
const [query, setQuery] = useState('');
|
|
22
|
+
const [title, setTitle] = useState('');
|
|
23
|
+
const [body, setBody] = useState('');
|
|
24
|
+
const [tags, setTags] = useState('');
|
|
25
|
+
const [err, setErr] = useState('');
|
|
26
|
+
|
|
27
|
+
const persist = useCallback((next: MemoryNote[]) => {
|
|
28
|
+
setNotes(next);
|
|
29
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
api<MissionsResponse>('/api/missions').then((data) => {
|
|
34
|
+
setMissions(data.missions);
|
|
35
|
+
setErr('');
|
|
36
|
+
const next = mergeMissionNotes(notes, data.missions);
|
|
37
|
+
if (next !== notes) persist(next);
|
|
38
|
+
}).catch((e) => setErr((e as Error).message));
|
|
39
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
|
+
}, [refreshTick]);
|
|
41
|
+
|
|
42
|
+
const filtered = useMemo(() => {
|
|
43
|
+
const q = query.trim().toLowerCase();
|
|
44
|
+
if (!q) return notes;
|
|
45
|
+
return notes.filter((note) => [
|
|
46
|
+
note.title,
|
|
47
|
+
note.body,
|
|
48
|
+
note.source,
|
|
49
|
+
note.mission_id ?? '',
|
|
50
|
+
...note.tags,
|
|
51
|
+
].some((v) => v.toLowerCase().includes(q)));
|
|
52
|
+
}, [notes, query]);
|
|
53
|
+
|
|
54
|
+
const add = () => {
|
|
55
|
+
if (!title.trim()) return;
|
|
56
|
+
persist([{
|
|
57
|
+
id: `note-${Date.now()}`,
|
|
58
|
+
title: title.trim(),
|
|
59
|
+
body: body.trim(),
|
|
60
|
+
tags: tags.split(',').map((t) => t.trim()).filter(Boolean),
|
|
61
|
+
created_at: new Date().toISOString(),
|
|
62
|
+
source: 'operator',
|
|
63
|
+
}, ...notes]);
|
|
64
|
+
setTitle('');
|
|
65
|
+
setBody('');
|
|
66
|
+
setTags('');
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const remove = (id: string) => persist(notes.filter((note) => note.id !== id));
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
73
|
+
<p className="hint">
|
|
74
|
+
Operator memory for Cockpit: local notes plus automatic Mission completion notes from durable
|
|
75
|
+
Mission Control state. Notes stay in this browser profile; AIWG source files are not modified.
|
|
76
|
+
</p>
|
|
77
|
+
{err && <p className="err">Mission memory sync failed: {err}</p>}
|
|
78
|
+
<section className="memory-layout">
|
|
79
|
+
<form className="memory-form" onSubmit={(e) => { e.preventDefault(); add(); }} aria-label="Add memory note">
|
|
80
|
+
<h2>Add Note</h2>
|
|
81
|
+
<label>Title<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Note title" /></label>
|
|
82
|
+
<label>Body<textarea value={body} onChange={(e) => setBody(e.target.value)} placeholder="What should future you remember?" rows={4} /></label>
|
|
83
|
+
<label>Tags<input value={tags} onChange={(e) => setTags(e.target.value)} placeholder="cockpit, release, risk" /></label>
|
|
84
|
+
<button type="submit" disabled={!title.trim()}>Save Note</button>
|
|
85
|
+
</form>
|
|
86
|
+
<section className="memory-list" aria-label="Memory notes">
|
|
87
|
+
<div className="section-toolbar">
|
|
88
|
+
<div>
|
|
89
|
+
<h2>Notes</h2>
|
|
90
|
+
<p className="hint">{notes.length} saved · {missions.filter((m) => m.terminal).length} terminal mission(s) observed</p>
|
|
91
|
+
</div>
|
|
92
|
+
<input aria-label="Search memory notes" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search notes" />
|
|
93
|
+
</div>
|
|
94
|
+
{!filtered.length ? <p className="empty">{query ? 'No notes match your search.' : 'No notes yet.'}</p> : (
|
|
95
|
+
<div className="note-grid">
|
|
96
|
+
{filtered.map((note) => (
|
|
97
|
+
<article className="note-card" key={note.id}>
|
|
98
|
+
<header>
|
|
99
|
+
<h3>{note.title}</h3>
|
|
100
|
+
<button aria-label={`Delete note ${note.title}`} onClick={() => remove(note.id)}>Remove</button>
|
|
101
|
+
</header>
|
|
102
|
+
{note.body && <p>{note.body}</p>}
|
|
103
|
+
<footer>
|
|
104
|
+
<time dateTime={note.created_at}>{new Date(note.created_at).toLocaleString()}</time>
|
|
105
|
+
{note.mission_id && <code>{fmtId(note.mission_id)}</code>}
|
|
106
|
+
{note.tags.map((tag) => <span className="tag" key={tag}>{tag}</span>)}
|
|
107
|
+
</footer>
|
|
108
|
+
</article>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</section>
|
|
113
|
+
</section>
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readNotes(): MemoryNote[] {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
|
|
121
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
122
|
+
} catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function mergeMissionNotes(notes: MemoryNote[], missions: MissionProjection[]) {
|
|
128
|
+
const existing = new Set(notes.map((note) => note.mission_id).filter(Boolean));
|
|
129
|
+
const additions = missions
|
|
130
|
+
.filter((mission) => mission.terminal && !existing.has(mission.id))
|
|
131
|
+
.map((mission): MemoryNote => ({
|
|
132
|
+
id: `mission-note-${mission.id}`,
|
|
133
|
+
title: mission.title.slice(0, 96),
|
|
134
|
+
body: [
|
|
135
|
+
`Status: ${mission.status}`,
|
|
136
|
+
mission.completion ? `Completion: ${mission.completion}` : '',
|
|
137
|
+
mission.error ? `Error: ${mission.error}` : '',
|
|
138
|
+
].filter(Boolean).join('\n'),
|
|
139
|
+
tags: ['mission', mission.status, mission.source].filter(Boolean),
|
|
140
|
+
created_at: mission.completed_at ?? new Date().toISOString(),
|
|
141
|
+
mission_id: mission.id,
|
|
142
|
+
source: 'mission',
|
|
143
|
+
}));
|
|
144
|
+
return additions.length ? [...additions, ...notes] : notes;
|
|
145
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { api } from '../api';
|
|
3
|
+
import { fmtId } from '../util';
|
|
4
|
+
import type { MissionProjection, MissionSession, MissionsResponse } from '../types';
|
|
5
|
+
|
|
6
|
+
export function Missions({ refreshTick = 0 }: { refreshTick?: number }) {
|
|
7
|
+
const [data, setData] = useState<MissionsResponse | null>(null);
|
|
8
|
+
const [err, setErr] = useState('');
|
|
9
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
10
|
+
|
|
11
|
+
const load = useCallback(() => {
|
|
12
|
+
api<MissionsResponse>('/api/missions')
|
|
13
|
+
.then((d) => {
|
|
14
|
+
setData(d);
|
|
15
|
+
setErr('');
|
|
16
|
+
setSelected((current) => current ?? d.sessions[0]?.id ?? null);
|
|
17
|
+
})
|
|
18
|
+
.catch((e) => setErr((e as Error).message));
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
useEffect(() => { load(); }, [load, refreshTick]);
|
|
22
|
+
|
|
23
|
+
if (err) return <p className="err">Could not load Missions: {err}</p>;
|
|
24
|
+
if (!data) return <p className="empty">Loading...</p>;
|
|
25
|
+
|
|
26
|
+
const active = data.sessions.find((s) => s.id === selected) ?? data.sessions[0] ?? null;
|
|
27
|
+
const totals = summarize(data.missions);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<p className="hint">
|
|
32
|
+
Durable Mission Control state and live executor work share one projection. Cockpit reads
|
|
33
|
+
<code> aiwg mc </code> sessions and audit logs; the conductor remains the owner of durability.
|
|
34
|
+
</p>
|
|
35
|
+
<div className="mission-summary" aria-label="Mission status summary">
|
|
36
|
+
<span><strong>{data.count}</strong> total</span>
|
|
37
|
+
<span><strong>{totals.active}</strong> active</span>
|
|
38
|
+
<span><strong>{totals.awaiting}</strong> awaiting approval</span>
|
|
39
|
+
<span><strong>{totals.terminal}</strong> terminal</span>
|
|
40
|
+
</div>
|
|
41
|
+
{!data.sessions.length ? <p className="empty">No Mission Control sessions or live executor tasks are visible yet.</p> : (
|
|
42
|
+
<div className="missions-layout">
|
|
43
|
+
<aside className="mission-sessions" aria-label="Mission sessions">
|
|
44
|
+
{data.sessions.map((s) => (
|
|
45
|
+
<button
|
|
46
|
+
key={s.id}
|
|
47
|
+
className={s.id === active?.id ? 'selected' : ''}
|
|
48
|
+
onClick={() => setSelected(s.id)}
|
|
49
|
+
aria-pressed={s.id === active?.id}
|
|
50
|
+
>
|
|
51
|
+
<span>{s.name}</span>
|
|
52
|
+
<small>{s.source} · {s.missions.length}</small>
|
|
53
|
+
</button>
|
|
54
|
+
))}
|
|
55
|
+
</aside>
|
|
56
|
+
{active && <MissionSessionView session={active} />}
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function MissionSessionView({ session }: { session: MissionSession }) {
|
|
64
|
+
return (
|
|
65
|
+
<section className="mission-detail" aria-label={`${session.name} missions`}>
|
|
66
|
+
<div className="mission-detail-head">
|
|
67
|
+
<div>
|
|
68
|
+
<h2>{session.name}</h2>
|
|
69
|
+
<p>{session.source} · {session.state}{session.updated_at ? ` · updated ${new Date(session.updated_at).toLocaleString()}` : ''}</p>
|
|
70
|
+
</div>
|
|
71
|
+
<span className="badge">{session.audit_count} audit events</span>
|
|
72
|
+
</div>
|
|
73
|
+
{!session.missions.length ? <p className="empty">This session has no missions.</p> : (
|
|
74
|
+
<table>
|
|
75
|
+
<caption>{session.missions.length} mission projection(s)</caption>
|
|
76
|
+
<thead>
|
|
77
|
+
<tr><th scope="col">Mission</th><th scope="col">Status</th><th scope="col">Source</th><th scope="col">Loop</th><th scope="col">Backing</th></tr>
|
|
78
|
+
</thead>
|
|
79
|
+
<tbody>
|
|
80
|
+
{session.missions.map((m) => (
|
|
81
|
+
<tr key={m.id}>
|
|
82
|
+
<td>
|
|
83
|
+
<strong>{m.title}</strong>
|
|
84
|
+
{m.completion && <small className="block">Done when: {m.completion}</small>}
|
|
85
|
+
{m.error && <small className="block err">{m.error}</small>}
|
|
86
|
+
</td>
|
|
87
|
+
<td><span className={`state ${statusClass(m.status)}`}><span className="dot" aria-hidden="true" />{m.status}</span></td>
|
|
88
|
+
<td>{m.source}</td>
|
|
89
|
+
<td>{loopText(m)}</td>
|
|
90
|
+
<td>{backingText(m)}</td>
|
|
91
|
+
</tr>
|
|
92
|
+
))}
|
|
93
|
+
</tbody>
|
|
94
|
+
</table>
|
|
95
|
+
)}
|
|
96
|
+
{session.audit_tail.length > 0 && (
|
|
97
|
+
<section className="audit-tail" aria-label="Mission audit tail">
|
|
98
|
+
<h3>Audit Tail</h3>
|
|
99
|
+
{session.audit_tail.map((event, i) => (
|
|
100
|
+
<p key={`${event.ts ?? 'event'}-${i}`}>
|
|
101
|
+
<code>{event.ts ? new Date(event.ts).toLocaleTimeString() : 'event'}</code>
|
|
102
|
+
{' '}{String(event.event ?? 'mission_event')}
|
|
103
|
+
{event.missionId || event.mission_id ? <> · <code>{fmtId(String(event.missionId ?? event.mission_id))}</code></> : null}
|
|
104
|
+
</p>
|
|
105
|
+
))}
|
|
106
|
+
</section>
|
|
107
|
+
)}
|
|
108
|
+
</section>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function summarize(missions: MissionProjection[]) {
|
|
113
|
+
return missions.reduce((acc, mission) => {
|
|
114
|
+
if (mission.status === 'awaiting-approval' || mission.status === 'input-required') acc.awaiting += 1;
|
|
115
|
+
if (mission.terminal) acc.terminal += 1;
|
|
116
|
+
else acc.active += 1;
|
|
117
|
+
return acc;
|
|
118
|
+
}, { active: 0, awaiting: 0, terminal: 0 });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function statusClass(status: string) {
|
|
122
|
+
return status.replace(/[^a-z0-9_-]/gi, '-');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function loopText(mission: MissionProjection) {
|
|
126
|
+
if (mission.loop === undefined && !mission.max_iterations) return '-';
|
|
127
|
+
return `${mission.loop ?? 0}/${mission.max_iterations ?? '?'}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function backingText(mission: MissionProjection) {
|
|
131
|
+
if (mission.ralph_loop_id) return `Ralph ${fmtId(mission.ralph_loop_id)}`;
|
|
132
|
+
if (mission.task_id && mission.instance_id) return `${fmtId(mission.instance_id)} / ${fmtId(mission.task_id)}`;
|
|
133
|
+
if (mission.target_agent) return fmtId(mission.target_agent);
|
|
134
|
+
return '-';
|
|
135
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { api } from '../api';
|
|
3
|
+
import { fmtId } from '../util';
|
|
4
|
+
import type { Cost, EventsSnapshot, MissionsResponse } from '../types';
|
|
5
|
+
|
|
6
|
+
type Filter = 'all' | 'mission' | 'task' | 'approval' | 'session' | 'inventory';
|
|
7
|
+
|
|
8
|
+
export function Telemetry({ refreshTick = 0 }: { refreshTick?: number }) {
|
|
9
|
+
const [events, setEvents] = useState<EventsSnapshot | null>(null);
|
|
10
|
+
const [missions, setMissions] = useState<MissionsResponse | null>(null);
|
|
11
|
+
const [cost, setCost] = useState<Cost | null>(null);
|
|
12
|
+
const [filter, setFilter] = useState<Filter>('all');
|
|
13
|
+
const [err, setErr] = useState('');
|
|
14
|
+
|
|
15
|
+
const load = useCallback(() => {
|
|
16
|
+
Promise.all([
|
|
17
|
+
api<EventsSnapshot>('/api/events/snapshot'),
|
|
18
|
+
api<MissionsResponse>('/api/missions').catch(() => null),
|
|
19
|
+
api<Cost>('/api/cost').catch(() => null),
|
|
20
|
+
]).then(([eventData, missionData, costData]) => {
|
|
21
|
+
setEvents(eventData);
|
|
22
|
+
setMissions(missionData);
|
|
23
|
+
setCost(costData);
|
|
24
|
+
setErr('');
|
|
25
|
+
}).catch((e) => setErr((e as Error).message));
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
useEffect(() => { load(); }, [load, refreshTick]);
|
|
29
|
+
|
|
30
|
+
if (err) return <p className="err">Could not load telemetry: {err}</p>;
|
|
31
|
+
if (!events) return <p className="empty">Loading...</p>;
|
|
32
|
+
|
|
33
|
+
const visible = filter === 'all' ? events.events : events.events.filter((e) => e.type.startsWith(filter));
|
|
34
|
+
const missionTotals = summarizeMissions(missions);
|
|
35
|
+
const tokenTotal = cost ? cost.total.input_tokens + cost.total.output_tokens : 0;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<p className="hint">
|
|
40
|
+
Live Cockpit telemetry from the unified event model. This replaces the legacy dashboard feed with
|
|
41
|
+
Bridge-backed inventory, session, task, approval, Mission, and cost posture.
|
|
42
|
+
</p>
|
|
43
|
+
<section className="telemetry-kpis" aria-label="Telemetry key performance indicators">
|
|
44
|
+
<article><span>Events</span><strong>{events.count}</strong><small>{events.source}</small></article>
|
|
45
|
+
<article><span>Active Missions</span><strong>{missionTotals.active}</strong><small>{missionTotals.terminal} terminal</small></article>
|
|
46
|
+
<article><span>Approvals</span><strong>{missionTotals.awaiting}</strong><small>awaiting operator input</small></article>
|
|
47
|
+
<article><span>Spend</span><strong>{cost ? `$${cost.total.usd.toFixed(2)}` : '-'}</strong><small>{tokenTotal ? `${tokenTotal.toLocaleString()} tokens` : 'cost route unavailable'}</small></article>
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
<div className="controls" role="group" aria-label="Filter telemetry events">
|
|
51
|
+
{(['all', 'mission', 'task', 'approval', 'session', 'inventory'] as const).map((f) => (
|
|
52
|
+
<button key={f} aria-pressed={filter === f} onClick={() => setFilter(f)}>{f}</button>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{!visible.length ? <p className="empty">No events match this filter.</p> : (
|
|
57
|
+
<table>
|
|
58
|
+
<caption>{visible.length} event(s), fetched {new Date(events.fetched_at).toLocaleTimeString()}</caption>
|
|
59
|
+
<thead><tr><th scope="col">Time</th><th scope="col">Type</th><th scope="col">Subject</th><th scope="col">State</th><th scope="col">Source</th></tr></thead>
|
|
60
|
+
<tbody>
|
|
61
|
+
{visible.map((e) => (
|
|
62
|
+
<tr key={e.id}>
|
|
63
|
+
<td><time dateTime={e.ts}>{new Date(e.ts).toLocaleTimeString()}</time></td>
|
|
64
|
+
<td><span className="badge">{e.type}</span></td>
|
|
65
|
+
<td><code title={e.subject}>{fmtId(e.subject)}</code></td>
|
|
66
|
+
<td>{e.state ? <span className={`state ${e.state}`}><span className="dot" aria-hidden="true" />{e.state}</span> : '-'}</td>
|
|
67
|
+
<td>{e.source}</td>
|
|
68
|
+
</tr>
|
|
69
|
+
))}
|
|
70
|
+
</tbody>
|
|
71
|
+
</table>
|
|
72
|
+
)}
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function summarizeMissions(data: MissionsResponse | null) {
|
|
78
|
+
const summary = { active: 0, terminal: 0, awaiting: 0 };
|
|
79
|
+
for (const mission of data?.missions ?? []) {
|
|
80
|
+
if (mission.status === 'awaiting-approval' || mission.status === 'input-required') summary.awaiting += 1;
|
|
81
|
+
if (mission.terminal) summary.terminal += 1;
|
|
82
|
+
else summary.active += 1;
|
|
83
|
+
}
|
|
84
|
+
return summary;
|
|
85
|
+
}
|
package/web/src/styles.css
CHANGED
|
@@ -18,6 +18,7 @@ header .meta { margin-left:0; color:var(--muted); font-size:13px; }
|
|
|
18
18
|
[role=tab]:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
|
|
19
19
|
main { padding:22px; }
|
|
20
20
|
button, select, input { background:var(--panel); color:var(--fg); border:1px solid var(--line); border-radius:7px; padding:7px 12px; font:inherit; }
|
|
21
|
+
textarea { background:var(--panel); color:var(--fg); border:1px solid var(--line); border-radius:7px; padding:7px 12px; font:inherit; resize:vertical; }
|
|
21
22
|
button { cursor:pointer; }
|
|
22
23
|
button:hover:not(:disabled) { border-color:var(--accent); }
|
|
23
24
|
button:disabled { opacity:.45; cursor:not-allowed; }
|
|
@@ -62,11 +63,60 @@ code { font:13px ui-monospace, monospace; color:var(--muted); }
|
|
|
62
63
|
.response-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:12px; align-items:start; border:1px solid var(--warn); border-radius:8px; background:rgba(253,230,138,.08); padding:12px 14px; }
|
|
63
64
|
.response-item p { margin:5px 0; color:var(--fg); white-space:pre-wrap; overflow-wrap:anywhere; }
|
|
64
65
|
.response-item span { color:var(--muted); font-size:12px; }
|
|
66
|
+
.mission-summary { display:flex; gap:10px; flex-wrap:wrap; margin:14px 0; }
|
|
67
|
+
.mission-summary span { border:1px solid var(--line); border-radius:8px; background:var(--panel); padding:8px 12px; color:var(--muted); }
|
|
68
|
+
.mission-summary strong { color:var(--fg); }
|
|
69
|
+
.missions-layout { display:grid; grid-template-columns:minmax(220px, 300px) minmax(0, 1fr); gap:14px; align-items:start; margin-top:14px; }
|
|
70
|
+
.mission-sessions { display:grid; gap:8px; }
|
|
71
|
+
.mission-sessions button { display:grid; gap:2px; text-align:left; border-radius:8px; }
|
|
72
|
+
.mission-sessions button.selected { border-color:var(--accent); background:rgba(94,234,212,.08); }
|
|
73
|
+
.mission-sessions small { color:var(--muted); }
|
|
74
|
+
.mission-detail { min-width:0; display:grid; gap:12px; }
|
|
75
|
+
.mission-detail-head { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; border:1px solid var(--line); border-radius:8px; background:var(--panel); padding:14px; }
|
|
76
|
+
.mission-detail-head h2 { margin:0; font-size:18px; }
|
|
77
|
+
.mission-detail-head p { margin:3px 0 0; color:var(--muted); font-size:13px; }
|
|
78
|
+
.block { display:block; margin-top:4px; color:var(--muted); font-size:12px; overflow-wrap:anywhere; }
|
|
79
|
+
.audit-tail { border:1px solid var(--line); border-radius:8px; background:var(--panel); padding:12px 14px; }
|
|
80
|
+
.audit-tail h3 { margin:0 0 8px; font-size:14px; }
|
|
81
|
+
.audit-tail p { margin:5px 0; color:var(--muted); font-size:13px; }
|
|
82
|
+
.telemetry-kpis { display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:10px; margin:14px 0; }
|
|
83
|
+
.telemetry-kpis article { display:grid; gap:4px; border:1px solid var(--line); border-radius:8px; background:var(--panel); padding:12px 14px; }
|
|
84
|
+
.telemetry-kpis span, .telemetry-kpis small { color:var(--muted); font-size:12px; }
|
|
85
|
+
.telemetry-kpis strong { font-size:24px; }
|
|
86
|
+
.controls button[aria-pressed=true] { border-color:var(--accent); color:var(--accent); }
|
|
87
|
+
.memory-layout { display:grid; grid-template-columns:minmax(260px, 360px) minmax(0, 1fr); gap:14px; align-items:start; margin-top:14px; }
|
|
88
|
+
.memory-form { display:grid; gap:10px; border:1px solid var(--line); border-radius:8px; background:var(--panel); padding:14px; }
|
|
89
|
+
.memory-form h2 { margin:0; font-size:18px; }
|
|
90
|
+
.memory-form label { display:grid; gap:5px; color:var(--muted); font-size:13px; }
|
|
91
|
+
.memory-form input, .memory-form textarea { width:100%; }
|
|
92
|
+
.memory-list { min-width:0; }
|
|
93
|
+
.note-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:10px; }
|
|
94
|
+
.note-card { display:grid; gap:8px; border:1px solid var(--line); border-radius:8px; background:var(--panel); padding:12px; }
|
|
95
|
+
.note-card header { position:static; display:flex; align-items:flex-start; justify-content:space-between; gap:10px; padding:0; border:0; background:transparent; }
|
|
96
|
+
.note-card h3 { margin:0; font-size:15px; }
|
|
97
|
+
.note-card p { margin:0; color:var(--muted); white-space:pre-wrap; overflow-wrap:anywhere; }
|
|
98
|
+
.note-card footer { display:flex; gap:6px; align-items:center; flex-wrap:wrap; color:var(--muted); font-size:12px; }
|
|
65
99
|
.grid2 { display:grid; grid-template-columns:minmax(260px,1fr) 1.4fr; gap:16px; align-items:start; }
|
|
100
|
+
.index-live { border:1px solid var(--line); border-radius:8px; background:rgba(17,24,39,.72); padding:14px; margin:14px 0 18px; }
|
|
101
|
+
.index-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:8px; margin-bottom:12px; }
|
|
102
|
+
.index-graph { border:1px solid var(--line); border-radius:8px; background:var(--panel); padding:10px 12px; }
|
|
103
|
+
.index-graph.missing { border-color:var(--warn); }
|
|
104
|
+
.index-graph p { margin:5px 0 0; color:var(--muted); font-size:13px; }
|
|
105
|
+
.index-query { display:grid; grid-template-columns:minmax(220px,1fr) auto; gap:10px; align-items:end; margin-top:10px; }
|
|
106
|
+
.index-query label { display:grid; gap:5px; color:var(--muted); font-size:13px; }
|
|
107
|
+
.index-results { display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:8px; margin-top:12px; }
|
|
108
|
+
.index-results article { border:1px solid var(--line); border-radius:8px; background:var(--panel); padding:10px 12px; }
|
|
109
|
+
.index-results p { margin:5px 0; color:var(--muted); font-size:13px; overflow-wrap:anywhere; }
|
|
110
|
+
.index-results small { color:var(--muted); }
|
|
66
111
|
.act { text-align:left; }
|
|
67
112
|
.action-cluster { display:inline-flex; align-items:stretch; gap:4px; }
|
|
68
113
|
.action-cluster .act { border-radius:7px 0 0 7px; }
|
|
69
114
|
.copy-command { border-radius:0 7px 7px 0; color:var(--muted); font-size:12px; padding-inline:9px; }
|
|
115
|
+
.contrib-layout { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:14px; margin-top:16px; }
|
|
116
|
+
.contrib-grid { display:grid; gap:10px; }
|
|
117
|
+
.contrib-item { display:grid; gap:8px; align-items:start; border:1px solid var(--line); border-radius:8px; background:var(--panel); padding:12px; }
|
|
118
|
+
.contrib-item p { margin:0; color:var(--muted); font-size:13px; overflow-wrap:anywhere; }
|
|
119
|
+
.workflow-steps { display:flex; gap:8px; flex-wrap:wrap; }
|
|
70
120
|
/* Tenor-style capability grid (modeled on fortemi-react SearchResults) */
|
|
71
121
|
.capgrid { display:grid; grid-template-columns:repeat(auto-fill,minmax(230px,1fr)); gap:10px; }
|
|
72
122
|
.capcard { display:block; text-align:left; background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:11px 13px; cursor:pointer; }
|
|
@@ -234,7 +284,7 @@ code { font:13px ui-monospace, monospace; color:var(--muted); }
|
|
|
234
284
|
@media (max-width: 820px) {
|
|
235
285
|
header { position:static; }
|
|
236
286
|
.top-status { width:100%; margin-left:0; }
|
|
237
|
-
.wall-hero, .guided-start, .radial-wall, .control-planner { grid-template-columns:1fr; align-items:start; }
|
|
287
|
+
.wall-hero, .guided-start, .radial-wall, .control-planner, .missions-layout, .memory-layout { grid-template-columns:1fr; align-items:start; }
|
|
238
288
|
.hero-actions { justify-content:flex-start; }
|
|
239
289
|
.telemetry-strip { grid-template-columns:repeat(2,minmax(120px,1fr)); }
|
|
240
290
|
.planner-grid { grid-template-columns:1fr; }
|
package/web/src/types.ts
CHANGED
|
@@ -38,11 +38,78 @@ export interface RunningTask { instance_id: string; task_id: string; state: stri
|
|
|
38
38
|
export interface SessionInfo { id: string; instance_id: string; seq?: number; members?: number; has_controller?: boolean; attach_url: string; mode?: 'direct' | 'managed'; backend?: string; session_name?: string; sessionName?: string; session_class?: string; session_backend?: string; role_policy?: string; replay?: boolean; keyframe?: boolean; controllers?: number; observers?: number }
|
|
39
39
|
export interface Approval { id: string; instance_id: string; prompt: string; risk: string; status: string }
|
|
40
40
|
export interface ResponseNeeded { id: string; instance_id: string; prompt: string; source: string; status: string; attach_url?: string | null }
|
|
41
|
+
export interface MissionAuditEvent { event?: string; ts?: string; missionId?: string; mission_id?: string; objective?: string; [key: string]: unknown }
|
|
42
|
+
export interface MissionProjection {
|
|
43
|
+
id: string;
|
|
44
|
+
session_id: string;
|
|
45
|
+
source: string;
|
|
46
|
+
title: string;
|
|
47
|
+
completion?: string;
|
|
48
|
+
status: string;
|
|
49
|
+
loop?: number;
|
|
50
|
+
max_iterations?: number;
|
|
51
|
+
priority?: string;
|
|
52
|
+
mode?: string;
|
|
53
|
+
target_agent?: string;
|
|
54
|
+
ralph_loop_id?: string;
|
|
55
|
+
ralph_pid?: number;
|
|
56
|
+
started_at?: string;
|
|
57
|
+
completed_at?: string;
|
|
58
|
+
error?: string;
|
|
59
|
+
instance_id?: string;
|
|
60
|
+
task_id?: string;
|
|
61
|
+
tenant?: string;
|
|
62
|
+
risk?: string;
|
|
63
|
+
terminal?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface MissionSession {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
state: string;
|
|
69
|
+
source: string;
|
|
70
|
+
created_at?: string;
|
|
71
|
+
updated_at?: string;
|
|
72
|
+
max_missions?: number;
|
|
73
|
+
audit_count: number;
|
|
74
|
+
audit_tail: MissionAuditEvent[];
|
|
75
|
+
missions: MissionProjection[];
|
|
76
|
+
}
|
|
77
|
+
export interface MissionsResponse {
|
|
78
|
+
source: string;
|
|
79
|
+
fetched_at: string;
|
|
80
|
+
count: number;
|
|
81
|
+
sessions: MissionSession[];
|
|
82
|
+
missions: MissionProjection[];
|
|
83
|
+
}
|
|
84
|
+
export interface UnifiedEvent {
|
|
85
|
+
id: string;
|
|
86
|
+
type: string;
|
|
87
|
+
source: string;
|
|
88
|
+
subject: string;
|
|
89
|
+
state?: string;
|
|
90
|
+
severity?: string;
|
|
91
|
+
ts: string;
|
|
92
|
+
ref?: Record<string, string | undefined>;
|
|
93
|
+
}
|
|
94
|
+
export interface EventsSnapshot {
|
|
95
|
+
source: string;
|
|
96
|
+
fetched_at: string;
|
|
97
|
+
count: number;
|
|
98
|
+
events: UnifiedEvent[];
|
|
99
|
+
}
|
|
41
100
|
export interface InstanceCost { instance_id: string; tenant: string; input_tokens: number; output_tokens: number; usd: number }
|
|
42
101
|
export interface Cost { total: { input_tokens: number; output_tokens: number; usd: number }; per_instance: InstanceCost[] }
|
|
43
102
|
export interface Loadout { id: string; label: string; description?: string; runtimes?: string[] }
|
|
44
103
|
export interface ExecutorCapabilities { status: string; source?: string | null; host_runtime_enabled: boolean; raw_status?: string; error?: string }
|
|
45
104
|
export interface CapabilityResult { path: string; type: string; title?: string; capability?: string; score?: number; name: string; triggers?: string[] }
|
|
46
105
|
export interface ContribAction { id: string; title: string; icon?: string; group?: string; source: string; inject: { command: string; target?: string; needs_args?: boolean; args_hint?: string } }
|
|
106
|
+
export interface ContribScreen { id: string; title: string; source: string; contribution: string }
|
|
107
|
+
export interface ContribWorkflowStep { action: string; label?: string }
|
|
108
|
+
export interface ContribWorkflow { id: string; title: string; description?: string; source: string; steps: ContribWorkflowStep[] }
|
|
109
|
+
export interface ContributionsResponse { actions: ContribAction[]; screens: ContribScreen[]; workflows: ContribWorkflow[] }
|
|
110
|
+
export interface IndexGraphStatus { name: string; origin: string; shared: boolean; defaultBuild: boolean; location: string; built: boolean; builtAt: string | null; ageHours: number | null; entries: number | null; missing: boolean }
|
|
111
|
+
export interface IndexStatusResponse { graphs: IndexGraphStatus[]; orphanIndexDirs: string[]; warnings: unknown[]; summary: { total: number; built: number; missing: number; orphans: number; warnings: number } }
|
|
112
|
+
export interface IndexQueryResult { path: string; title?: string; type?: string; phase?: string; summary?: string; score?: number }
|
|
113
|
+
export interface IndexQueryResponse { results?: IndexQueryResult[]; total?: number; query?: unknown; graph?: string | null; mode?: string }
|
|
47
114
|
export type Role = 'controller' | 'observer' | null;
|
|
48
115
|
export interface LibraryAsset { name: string; type: string; kind: 'dir' | 'file'; origin: string; source_path?: string }
|