@aiwg/cockpit 2026.7.11 → 2026.7.13
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 +80 -12
- package/bridge/src/server.mjs +301 -21
- package/bridge/src/smoke.mjs +3 -3
- package/package.json +2 -2
- package/runtime-docs/README.md +3 -2
- package/shell-core/keychain.mjs +35 -5
- package/vscode/extension.js +13 -26
- package/vscode/smoke.mjs +47 -0
- package/web/src/App.test.tsx +119 -1
- package/web/src/App.tsx +58 -27
- package/web/src/components/Actions.tsx +1 -1
- package/web/src/components/Inventory.tsx +61 -14
- package/web/src/components/Library.tsx +1 -1
- package/web/src/components/Sessions.test.tsx +248 -10
- package/web/src/components/Sessions.tsx +236 -65
- package/web/src/components/StartSessionModal.test.tsx +1 -1
- package/web/src/components/StartSessionModal.tsx +7 -1
- package/web/src/components/Welcome.tsx +11 -10
- package/web/src/sessionMonitor.test.tsx +85 -0
- package/web/src/sessionMonitor.ts +72 -0
- package/web/src/sessionRegistry.test.ts +112 -0
- package/web/src/sessionRegistry.ts +192 -0
- package/web/src/styles.css +20 -2
- package/web/src/types.ts +23 -1
- package/web/src/useSession.test.tsx +207 -7
- package/web/src/useSession.ts +353 -193
- package/web/src/util.ts +8 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { apiRaw } from './api';
|
|
3
|
+
import {
|
|
4
|
+
getSessionRegistrySnapshot,
|
|
5
|
+
updateRegistrySessionSnapshot,
|
|
6
|
+
useSessionRegistry,
|
|
7
|
+
} from './sessionRegistry';
|
|
8
|
+
|
|
9
|
+
interface ScreenSnapshotResponse {
|
|
10
|
+
text?: string;
|
|
11
|
+
snapshot?: string;
|
|
12
|
+
screen?: string;
|
|
13
|
+
content?: string;
|
|
14
|
+
seq?: number;
|
|
15
|
+
sequence?: number;
|
|
16
|
+
anchor_sequence?: number;
|
|
17
|
+
anchorSequence?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useSessionSnapshotMonitor(refreshMs = 12_000) {
|
|
21
|
+
useSessionRegistry();
|
|
22
|
+
// Sessions whose screen endpoint returned 404 (no server-side snapshot — e.g.
|
|
23
|
+
// host-runtime PTYs). Skip them on subsequent ticks so we don't re-request a
|
|
24
|
+
// known-missing endpoint every cycle and flood the console with 404s.
|
|
25
|
+
const noScreenRef = useRef<Set<string>>(new Set());
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
let cancelled = false;
|
|
29
|
+
let timer: number | undefined;
|
|
30
|
+
const schedule = (ms = refreshMs) => {
|
|
31
|
+
timer = window.setTimeout(tick, ms);
|
|
32
|
+
};
|
|
33
|
+
const tick = async () => {
|
|
34
|
+
if (cancelled) return;
|
|
35
|
+
if (document.hidden) {
|
|
36
|
+
schedule(refreshMs);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const entries = Object.values(getSessionRegistrySnapshot().entries)
|
|
40
|
+
.filter((entry) => !entry.attached)
|
|
41
|
+
.filter((entry) => entry.metadata.has_screen !== false)
|
|
42
|
+
.filter((entry) => !noScreenRef.current.has(`${entry.instanceId}:${entry.sessionId}`));
|
|
43
|
+
await Promise.all(entries.map(async (entry) => {
|
|
44
|
+
const key = `${entry.instanceId}:${entry.sessionId}`;
|
|
45
|
+
try {
|
|
46
|
+
const res = await apiRaw(
|
|
47
|
+
`/api/instances/${encodeURIComponent(entry.instanceId)}/sessions/${encodeURIComponent(entry.sessionId)}/screen`,
|
|
48
|
+
);
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
// No server-side snapshot for this session (e.g. host PTY). Stop
|
|
51
|
+
// polling it so it doesn't re-404 every cycle.
|
|
52
|
+
if (res.status === 404) noScreenRef.current.add(key);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const snapshot = (await res.json()) as ScreenSnapshotResponse;
|
|
56
|
+
const text = String(snapshot.text ?? snapshot.snapshot ?? snapshot.screen ?? snapshot.content ?? '');
|
|
57
|
+
const seq = snapshot.seq ?? snapshot.sequence ?? snapshot.anchor_sequence ?? snapshot.anchorSequence;
|
|
58
|
+
updateRegistrySessionSnapshot(entry.instanceId, entry.sessionId, text, { seq: typeof seq === 'number' ? seq : undefined });
|
|
59
|
+
} catch {
|
|
60
|
+
// Snapshot support is opportunistic. A transient network error should
|
|
61
|
+
// not break the active driven terminal or session list.
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
if (!cancelled) schedule(refreshMs);
|
|
65
|
+
};
|
|
66
|
+
schedule(0);
|
|
67
|
+
return () => {
|
|
68
|
+
cancelled = true;
|
|
69
|
+
if (timer !== undefined) window.clearTimeout(timer);
|
|
70
|
+
};
|
|
71
|
+
}, [refreshMs]);
|
|
72
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
getSessionRegistrySnapshot,
|
|
4
|
+
interactivePromptFrom,
|
|
5
|
+
markRegistrySessionViewed,
|
|
6
|
+
registryResponseNeededItems,
|
|
7
|
+
resetSessionRegistryForTest,
|
|
8
|
+
sessionRegistryKey,
|
|
9
|
+
setRegistryActiveSession,
|
|
10
|
+
subscribeSessionRegistry,
|
|
11
|
+
updateRegistrySessionSnapshot,
|
|
12
|
+
upsertRegistrySessions,
|
|
13
|
+
} from './sessionRegistry';
|
|
14
|
+
import type { SessionInfo } from './types';
|
|
15
|
+
|
|
16
|
+
const SESSION_A: SessionInfo = {
|
|
17
|
+
id: 'sess-a',
|
|
18
|
+
instance_id: 'inst-a',
|
|
19
|
+
attach_url: 'ws://x/agents/inst-a/sessions/sess-a/attach',
|
|
20
|
+
session_name: 'terminal-a',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const SESSION_B: SessionInfo = {
|
|
24
|
+
id: 'sess-b',
|
|
25
|
+
instance_id: 'inst-b',
|
|
26
|
+
attach_url: 'ws://x/agents/inst-b/sessions/sess-b/attach',
|
|
27
|
+
session_name: 'terminal-b',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
beforeEach(() => resetSessionRegistryForTest());
|
|
31
|
+
|
|
32
|
+
describe('sessionRegistry', () => {
|
|
33
|
+
it('keys entries by instance/session identity and preserves existing snapshot state on metadata refresh', () => {
|
|
34
|
+
upsertRegistrySessions([SESSION_A], '2026-07-06T20:00:00.000Z');
|
|
35
|
+
updateRegistrySessionSnapshot('inst-a', 'sess-a', 'hello\n', { now: '2026-07-06T20:00:01.000Z' });
|
|
36
|
+
upsertRegistrySessions([{ ...SESSION_A, session_name: 'renamed' }], '2026-07-06T20:00:02.000Z');
|
|
37
|
+
|
|
38
|
+
const entry = getSessionRegistrySnapshot().entries[sessionRegistryKey('inst-a', 'sess-a')];
|
|
39
|
+
expect(entry.metadata.session_name).toBe('renamed');
|
|
40
|
+
expect(entry.snapshot?.text).toBe('hello\n');
|
|
41
|
+
expect(entry.unread).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('tracks the active attached session without dropping background entries', () => {
|
|
45
|
+
upsertRegistrySessions([SESSION_A, SESSION_B]);
|
|
46
|
+
setRegistryActiveSession('inst-a', 'sess-a', '2026-07-06T20:01:00.000Z');
|
|
47
|
+
|
|
48
|
+
const snapshot = getSessionRegistrySnapshot();
|
|
49
|
+
expect(snapshot.activeKey).toBe('inst-a:sess-a');
|
|
50
|
+
expect(snapshot.entries['inst-a:sess-a'].attached).toBe(true);
|
|
51
|
+
expect(snapshot.entries['inst-b:sess-b'].attached).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('marks background snapshot changes unread and clears unread on view', () => {
|
|
55
|
+
upsertRegistrySessions([SESSION_A]);
|
|
56
|
+
updateRegistrySessionSnapshot('inst-a', 'sess-a', 'new output\n', { now: '2026-07-06T20:02:00.000Z' });
|
|
57
|
+
expect(getSessionRegistrySnapshot().entries['inst-a:sess-a'].unread).toBe(true);
|
|
58
|
+
|
|
59
|
+
markRegistrySessionViewed('inst-a', 'sess-a');
|
|
60
|
+
expect(getSessionRegistrySnapshot().entries['inst-a:sess-a'].unread).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('does not mark the actively attached session unread for its own snapshot changes', () => {
|
|
64
|
+
upsertRegistrySessions([SESSION_A]);
|
|
65
|
+
setRegistryActiveSession('inst-a', 'sess-a');
|
|
66
|
+
updateRegistrySessionSnapshot('inst-a', 'sess-a', 'driver output\n');
|
|
67
|
+
|
|
68
|
+
expect(getSessionRegistrySnapshot().entries['inst-a:sess-a'].unread).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('detects response-needed prompts from snapshot tails', () => {
|
|
72
|
+
upsertRegistrySessions([SESSION_A]);
|
|
73
|
+
updateRegistrySessionSnapshot('inst-a', 'sess-a', 'Deploy to prod? [y/N]\n', { now: '2026-07-06T20:03:00.000Z' });
|
|
74
|
+
|
|
75
|
+
const response = getSessionRegistrySnapshot().entries['inst-a:sess-a'].responseNeeded;
|
|
76
|
+
expect(response).toMatchObject({
|
|
77
|
+
needed: true,
|
|
78
|
+
prompt: 'Deploy to prod? [y/N]',
|
|
79
|
+
since: '2026-07-06T20:03:00.000Z',
|
|
80
|
+
source: 'snapshot',
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('projects response-needed entries for the approvals inbox', () => {
|
|
85
|
+
upsertRegistrySessions([SESSION_A]);
|
|
86
|
+
updateRegistrySessionSnapshot('inst-a', 'sess-a', 'Deploy to prod? [y/N]\n', { now: '2026-07-06T20:03:00.000Z' });
|
|
87
|
+
|
|
88
|
+
expect(registryResponseNeededItems(getSessionRegistrySnapshot())).toEqual([{
|
|
89
|
+
id: 'pty:inst-a:sess-a',
|
|
90
|
+
instance_id: 'inst-a',
|
|
91
|
+
prompt: 'Deploy to prod? [y/N]',
|
|
92
|
+
source: 'snapshot',
|
|
93
|
+
status: 'response-needed',
|
|
94
|
+
attach_url: 'ws://x/agents/inst-a/sessions/sess-a/attach',
|
|
95
|
+
}]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('notifies subscribers when registry state changes', () => {
|
|
99
|
+
const listener = vi.fn();
|
|
100
|
+
const unsubscribe = subscribeSessionRegistry(listener);
|
|
101
|
+
upsertRegistrySessions([SESSION_A]);
|
|
102
|
+
unsubscribe();
|
|
103
|
+
upsertRegistrySessions([SESSION_B]);
|
|
104
|
+
|
|
105
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('exports the prompt heuristic for monitor and PTY plumbing reuse', () => {
|
|
109
|
+
expect(interactivePromptFrom('Choose one\n1. yes\n2. no\n')).toContain('Choose one');
|
|
110
|
+
expect(interactivePromptFrom('plain build output\n')).toBe('');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
import type { ResponseNeeded, SessionInfo } from './types';
|
|
3
|
+
|
|
4
|
+
export interface RegistryResponseNeeded {
|
|
5
|
+
needed: boolean;
|
|
6
|
+
prompt: string;
|
|
7
|
+
since: string | null;
|
|
8
|
+
source: 'snapshot' | 'pty';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RegistrySnapshot {
|
|
12
|
+
text: string;
|
|
13
|
+
lines: string[];
|
|
14
|
+
fetchedAt: string;
|
|
15
|
+
seq?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RegistrySessionEntry {
|
|
19
|
+
key: string;
|
|
20
|
+
instanceId: string;
|
|
21
|
+
sessionId: string;
|
|
22
|
+
metadata: SessionInfo;
|
|
23
|
+
lastOutputAt: string | null;
|
|
24
|
+
unread: boolean;
|
|
25
|
+
responseNeeded: RegistryResponseNeeded;
|
|
26
|
+
snapshot: RegistrySnapshot | null;
|
|
27
|
+
attached: boolean;
|
|
28
|
+
updatedAt: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface RegistryState {
|
|
32
|
+
activeKey: string | null;
|
|
33
|
+
entries: Record<string, RegistrySessionEntry>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Listener = () => void;
|
|
37
|
+
|
|
38
|
+
const EMPTY_RESPONSE: RegistryResponseNeeded = { needed: false, prompt: '', since: null, source: 'snapshot' };
|
|
39
|
+
|
|
40
|
+
let state: RegistryState = { activeKey: null, entries: {} };
|
|
41
|
+
const listeners = new Set<Listener>();
|
|
42
|
+
|
|
43
|
+
export function sessionRegistryKey(instanceId: string, sessionId: string): string {
|
|
44
|
+
return `${instanceId}:${sessionId}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function sessionRegistryKeyFor(s: Pick<SessionInfo, 'instance_id' | 'id'>): string {
|
|
48
|
+
return sessionRegistryKey(s.instance_id, s.id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function subscribeSessionRegistry(listener: Listener): () => void {
|
|
52
|
+
listeners.add(listener);
|
|
53
|
+
return () => listeners.delete(listener);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getSessionRegistrySnapshot(): RegistryState {
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function useSessionRegistry(): RegistryState {
|
|
61
|
+
return useSyncExternalStore(subscribeSessionRegistry, getSessionRegistrySnapshot, getSessionRegistrySnapshot);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function registryResponseNeededItems(registry: RegistryState): ResponseNeeded[] {
|
|
65
|
+
return Object.values(registry.entries)
|
|
66
|
+
.filter((entry) => entry.responseNeeded.needed)
|
|
67
|
+
.map((entry) => ({
|
|
68
|
+
id: `pty:${entry.key}`,
|
|
69
|
+
instance_id: entry.instanceId,
|
|
70
|
+
prompt: entry.responseNeeded.prompt,
|
|
71
|
+
source: entry.responseNeeded.source,
|
|
72
|
+
status: 'response-needed',
|
|
73
|
+
attach_url: entry.metadata.attach_url,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resetSessionRegistryForTest() {
|
|
78
|
+
state = { activeKey: null, entries: {} };
|
|
79
|
+
emit();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function upsertRegistrySessions(sessions: SessionInfo[], now = new Date().toISOString()) {
|
|
83
|
+
if (!sessions.length) return;
|
|
84
|
+
update((prev) => {
|
|
85
|
+
const entries = { ...prev.entries };
|
|
86
|
+
for (const session of sessions) {
|
|
87
|
+
const key = sessionRegistryKeyFor(session);
|
|
88
|
+
const prior = entries[key];
|
|
89
|
+
entries[key] = {
|
|
90
|
+
key,
|
|
91
|
+
instanceId: session.instance_id,
|
|
92
|
+
sessionId: session.id,
|
|
93
|
+
metadata: { ...(prior?.metadata ?? {}), ...session },
|
|
94
|
+
lastOutputAt: prior?.lastOutputAt ?? null,
|
|
95
|
+
unread: prior?.unread ?? false,
|
|
96
|
+
responseNeeded: prior?.responseNeeded ?? EMPTY_RESPONSE,
|
|
97
|
+
snapshot: prior?.snapshot ?? null,
|
|
98
|
+
attached: prior?.attached ?? false,
|
|
99
|
+
updatedAt: now,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { ...prev, entries };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function setRegistryActiveSession(instanceId: string | null, sessionId: string | null, now = new Date().toISOString()) {
|
|
107
|
+
const nextActiveKey = instanceId && sessionId ? sessionRegistryKey(instanceId, sessionId) : null;
|
|
108
|
+
update((prev) => {
|
|
109
|
+
const entries = { ...prev.entries };
|
|
110
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
111
|
+
if (entry.attached !== (key === nextActiveKey)) entries[key] = { ...entry, attached: key === nextActiveKey, updatedAt: now };
|
|
112
|
+
}
|
|
113
|
+
return { activeKey: nextActiveKey, entries };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function markRegistrySessionViewed(instanceId: string, sessionId: string, now = new Date().toISOString()) {
|
|
118
|
+
const key = sessionRegistryKey(instanceId, sessionId);
|
|
119
|
+
updateEntry(key, (entry) => ({ ...entry, unread: false, updatedAt: now }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function updateRegistrySessionSnapshot(
|
|
123
|
+
instanceId: string,
|
|
124
|
+
sessionId: string,
|
|
125
|
+
snapshotText: string,
|
|
126
|
+
options: { seq?: number; now?: string; source?: RegistryResponseNeeded['source'] } = {},
|
|
127
|
+
) {
|
|
128
|
+
const key = sessionRegistryKey(instanceId, sessionId);
|
|
129
|
+
const now = options.now ?? new Date().toISOString();
|
|
130
|
+
updateEntry(key, (entry) => {
|
|
131
|
+
const previousText = entry.snapshot?.text ?? '';
|
|
132
|
+
const text = snapshotText;
|
|
133
|
+
const changed = text !== previousText;
|
|
134
|
+
const prompt = interactivePromptFrom(text);
|
|
135
|
+
return {
|
|
136
|
+
...entry,
|
|
137
|
+
lastOutputAt: changed ? now : entry.lastOutputAt,
|
|
138
|
+
unread: entry.attached ? false : entry.unread || changed,
|
|
139
|
+
responseNeeded: prompt
|
|
140
|
+
? { needed: true, prompt, since: entry.responseNeeded.prompt === prompt ? entry.responseNeeded.since : now, source: options.source ?? 'snapshot' }
|
|
141
|
+
: { ...EMPTY_RESPONSE, source: options.source ?? 'snapshot' },
|
|
142
|
+
snapshot: { text, lines: tailLines(text), fetchedAt: now, seq: options.seq },
|
|
143
|
+
updatedAt: now,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function updateEntry(key: string, mapper: (entry: RegistrySessionEntry) => RegistrySessionEntry) {
|
|
149
|
+
update((prev) => {
|
|
150
|
+
const current = prev.entries[key];
|
|
151
|
+
if (!current) return prev;
|
|
152
|
+
return { ...prev, entries: { ...prev.entries, [key]: mapper(current) } };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function update(mapper: (prev: RegistryState) => RegistryState) {
|
|
157
|
+
const next = mapper(state);
|
|
158
|
+
if (next === state) return;
|
|
159
|
+
state = next;
|
|
160
|
+
emit();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function emit() {
|
|
164
|
+
for (const listener of listeners) listener();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function tailLines(text: string, maxLines = 24): string[] {
|
|
168
|
+
return text.replace(/\r/g, '\n').split('\n').filter(Boolean).slice(-maxLines);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function stripAnsi(text: string): string {
|
|
172
|
+
return text
|
|
173
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
174
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, '')
|
|
175
|
+
.replace(/\x1b[()][A-Za-z0-9]/g, '');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function interactivePromptFrom(output: string): string {
|
|
179
|
+
const clean = stripAnsi(output).replace(/\r/g, '\n');
|
|
180
|
+
const lines = clean.split('\n').map((line) => line.trim()).filter(Boolean).slice(-24);
|
|
181
|
+
const text = lines.join('\n');
|
|
182
|
+
const promptPatterns = [
|
|
183
|
+
/Enter to select\b/i,
|
|
184
|
+
/(?:↑|up)\/(?:↓|down)|arrow keys|navigate/i,
|
|
185
|
+
/\bEsc to cancel\b/i,
|
|
186
|
+
/\b(?:y\/n|Y\/n|y\/N|\[y\/N\]|\[Y\/n\])\b/,
|
|
187
|
+
/\b(?:choose|select|pick) (?:one|an option|a number)\b/i,
|
|
188
|
+
/\?$/,
|
|
189
|
+
];
|
|
190
|
+
if (!promptPatterns.some((re) => re.test(text))) return '';
|
|
191
|
+
return lines.slice(-10).join('\n').slice(0, 900);
|
|
192
|
+
}
|
package/web/src/styles.css
CHANGED
|
@@ -38,13 +38,29 @@ code { font:13px ui-monospace, monospace; color:var(--muted); }
|
|
|
38
38
|
.badge.isolation-strong, .badge.trust-secure, .badge.daemon-available, .badge.daemon-detected { border-color:var(--ok); color:var(--ok); }
|
|
39
39
|
.badge.isolation-unknown, .badge.trust-unknown, .badge.daemon-unavailable { border-color:var(--idle); color:var(--muted); }
|
|
40
40
|
.cell-note { color:var(--muted); font-size:12px; margin-top:3px; max-width:260px; }
|
|
41
|
+
.inventory-table { table-layout:fixed; }
|
|
42
|
+
.inventory-table th, .inventory-table td { vertical-align:top; }
|
|
43
|
+
.inventory-table th:nth-child(1), .inventory-table td:nth-child(1) { width:17%; }
|
|
44
|
+
.inventory-table th:nth-child(2), .inventory-table td:nth-child(2) { width:15%; }
|
|
45
|
+
.inventory-table th:nth-child(3), .inventory-table td:nth-child(3) { width:13%; }
|
|
46
|
+
.inventory-table th:nth-child(4), .inventory-table td:nth-child(4) { width:11%; }
|
|
47
|
+
.inventory-table th:nth-child(5), .inventory-table td:nth-child(5) { width:13%; }
|
|
48
|
+
.inventory-table th:nth-child(6), .inventory-table td:nth-child(6) { width:7%; }
|
|
49
|
+
.inventory-table th:nth-child(7), .inventory-table td:nth-child(7) { width:6%; }
|
|
50
|
+
.inventory-table th:nth-child(8), .inventory-table td:nth-child(8) { width:18%; }
|
|
51
|
+
.inventory-table .badge { max-width:100%; white-space:normal; line-height:1.35; }
|
|
52
|
+
.inventory-table code { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
53
|
+
.inventory-table .cell-note { max-width:100%; overflow-wrap:anywhere; line-height:1.4; }
|
|
54
|
+
.runtime-cell .cell-note, .daemon-cell .cell-note { display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden; }
|
|
41
55
|
.state { display:inline-flex; align-items:center; gap:7px; }
|
|
42
56
|
.dot { width:8px; height:8px; border-radius:50%; flex:0 0 auto; background:var(--idle); }
|
|
43
|
-
.state.running .dot, .state.working .dot { background:var(--ok); } .state.provisioning .dot { background:var(--warn); }
|
|
57
|
+
.state.running .dot, .state.working .dot { background:var(--ok); } .state.provisioning .dot, .state.degraded .dot { background:var(--warn); }
|
|
44
58
|
.empty { color:var(--muted); padding:22px; } .err { color:var(--err); padding:12px 0; }
|
|
45
59
|
.controls { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:14px; }
|
|
46
60
|
.controls label { color:var(--muted); font-size:13px; }
|
|
47
|
-
.manage-actions { white-space:nowrap; display:flex; gap:8px; align-items:
|
|
61
|
+
.manage-actions { white-space:nowrap; display:flex; gap:8px; align-items:flex-start; justify-content:flex-start; }
|
|
62
|
+
.inventory-table .manage-actions button { min-width:74px; min-height:38px; padding-inline:10px; }
|
|
63
|
+
.inventory-table .manage-actions .cta { min-width:92px; }
|
|
48
64
|
.section-toolbar { display:flex; align-items:flex-end; justify-content:space-between; gap:14px; flex-wrap:wrap; margin-bottom:14px; }
|
|
49
65
|
.section-toolbar h2 { margin:0; font-size:20px; }
|
|
50
66
|
.empty-state { display:grid; gap:10px; justify-items:start; border:1px solid var(--line); border-radius:8px; padding:18px; background:var(--panel); }
|
|
@@ -329,6 +345,8 @@ code { font:13px ui-monospace, monospace; color:var(--muted); }
|
|
|
329
345
|
.nav-session-meta { font-size:11px; color:var(--muted); }
|
|
330
346
|
.nav-session .badge { padding:0 6px; font-size:10px; }
|
|
331
347
|
.nav-session .live-dot { border-color:var(--ok); color:var(--ok); }
|
|
348
|
+
.nav-session .unread { border-color:var(--accent2); color:var(--accent2); }
|
|
349
|
+
.nav-session .response { border-color:var(--warn); color:var(--warn); }
|
|
332
350
|
.nav-session-end { align-self:center; padding:4px 9px; color:var(--muted); border-color:transparent; background:none; line-height:1; }
|
|
333
351
|
.nav-session-end:hover:not(:disabled) { border-color:var(--err); color:var(--err); }
|
|
334
352
|
.nav-new-session { margin-top:6px; align-self:flex-start; }
|
package/web/src/types.ts
CHANGED
|
@@ -35,7 +35,29 @@ export interface Instance {
|
|
|
35
35
|
session_backends: SessionBackend[];
|
|
36
36
|
}
|
|
37
37
|
export interface RunningTask { instance_id: string; task_id: string; state: string; tenant: string; runtime_posture?: RuntimePosture; transport?: TransportPosture }
|
|
38
|
-
|
|
38
|
+
// Session model mirrors the agentic-sandbox v2 SessionEntry (management/src/http/sessions.rs,
|
|
39
|
+
// released in v2026.7.2). Cockpit consumes the v2 objects directly — no flat-field
|
|
40
|
+
// translation. `id` is the Cockpit primary key, set from the v2 `session_id`; `attach_url`
|
|
41
|
+
// is the data-plane URL the Bridge resolves. membership/liveness are the v2 sub-objects.
|
|
42
|
+
export interface SessionMembership { controllers: string[]; observers: string[]; attachment_count: number }
|
|
43
|
+
export interface SessionLiveness { agent_connected: boolean; has_screen: boolean; replay_newest_seq?: number | null; max_client_lag: number }
|
|
44
|
+
export interface SessionInfo {
|
|
45
|
+
id: string;
|
|
46
|
+
session_id?: string;
|
|
47
|
+
instance_id: string;
|
|
48
|
+
agent_id?: string;
|
|
49
|
+
session_name?: string;
|
|
50
|
+
session_type?: string;
|
|
51
|
+
session_backend?: string;
|
|
52
|
+
session_class?: string;
|
|
53
|
+
attach_url: string;
|
|
54
|
+
pty_ws_url?: string;
|
|
55
|
+
has_screen?: boolean;
|
|
56
|
+
role_policy?: string;
|
|
57
|
+
default_role?: string;
|
|
58
|
+
membership?: SessionMembership;
|
|
59
|
+
liveness?: SessionLiveness;
|
|
60
|
+
}
|
|
39
61
|
export interface Approval { id: string; instance_id: string; prompt: string; risk: string; status: string }
|
|
40
62
|
export interface ResponseNeeded { id: string; instance_id: string; prompt: string; source: string; status: string; attach_url?: string | null }
|
|
41
63
|
export interface MissionAuditEvent { event?: string; ts?: string; missionId?: string; mission_id?: string; objective?: string; [key: string]: unknown }
|