agentgui 1.0.943 → 1.0.945
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/AGENTS.md +7 -5
- package/CHANGELOG.md +13 -1
- package/package.json +1 -1
- package/site/app/index.html +6 -2
- package/site/app/js/app.js +77 -112
- package/site/app/vendor/anentrypoint-design/247420.css +40 -15
- package/site/app/vendor/anentrypoint-design/247420.js +11 -11
package/AGENTS.md
CHANGED
|
@@ -6,9 +6,11 @@ One surface. `server.js` serves `site/app/` under `BASE_URL` (default `/gm`) and
|
|
|
6
6
|
|
|
7
7
|
When `PASSWORD` env var is set, every HTTP route is gated by `lib/http-handler.js` accepting **Basic auth**, **`Authorization: Bearer <pwd>`**, OR **`?token=<pwd>`** query param (added 2026-05-26 so `EventSource` and direct deep-links work — neither can set headers). WS `/sync` requires `?token=` only. The HTML head script injects `window.__BASE_URL`, `window.__SERVER_VERSION`, and `window.__WS_TOKEN`; `site/app/js/backend.js` reads `__WS_TOKEN` and threads it onto every fetch (Bearer header) / EventSource (qs) / WebSocket (qs).
|
|
8
8
|
|
|
9
|
-
- `site/app/index.html` — shell + CSS
|
|
10
|
-
- `site/app/js/backend.js` — same-origin client (`DEFAULT_BACKEND = ''`); `?backend=` query override for cross-origin debugging
|
|
11
|
-
- `site/app/js/app.js` — webjsx view + state
|
|
9
|
+
- `site/app/index.html` — shell + CSS; imports `anentrypoint-design` from `https://unpkg.com/anentrypoint-design@latest` at runtime (always-latest; requires network — the prior offline guarantee is intentionally dropped)
|
|
10
|
+
- `site/app/js/backend.js` — same-origin WS/HTTP client (`DEFAULT_BACKEND = ''`); the transport glue that wires the kit; `?backend=` query override for cross-origin debugging
|
|
11
|
+
- `site/app/js/app.js` — webjsx view + state; renders the `AgentChat` kit (from `anentrypoint-design`) for the chat surface and wires agentgui's WS/ccsniff state as kit callbacks; history/settings remain agentgui-local. Exposes `window.__agentgui`
|
|
12
|
+
|
|
13
|
+
The chat GUI lives in the design kit, not in agentgui. The reusable multi-agent chat surface is the `AgentChat` component in `anentrypoint-design` (`src/components/agent-chat.js`); agentgui keeps only the transport glue (WS `backend.js`, ccsniff history wiring, agent orchestration) and passes state + callbacks into the kit. To change chat UI, edit the kit and push it (CI publishes to npm → unpkg `@latest`), not agentgui.
|
|
12
14
|
- `server.js` — boots ACP/agents/websocket plugins, mounts `createHistoryRouter()` from `ccsniff` at `/`, serves `site/app/` as static root
|
|
13
15
|
- Plugins kept (lib/plugins/): acp, agents, database, files, stream, websocket, workflow
|
|
14
16
|
|
|
@@ -145,9 +147,9 @@ Throttle renders via `requestAnimationFrame` to avoid event storm during burst l
|
|
|
145
147
|
|
|
146
148
|
The endpoints are served by ccsniff's Express router mounted in-process from `server.js`. No external proxy.
|
|
147
149
|
|
|
148
|
-
##
|
|
150
|
+
## Runtime CDN: GUI loaded from unpkg (2026-05-29)
|
|
149
151
|
|
|
150
|
-
The GUI
|
|
152
|
+
The GUI loads `anentrypoint-design` from `https://unpkg.com/anentrypoint-design@latest` at runtime (import map + CSS link in `index.html`) so agentgui always tracks the latest published kit with no hand-maintained vendored copy. This **requires a network connection** — the earlier offline guarantee was intentionally dropped in favour of always-latest. The DS itself fetches its fonts from `unpkg.com/anentrypoint-design@latest/dist/fonts/` (set in the kit's `scripts/build.mjs` `FONT_BASE`). The `site/app/vendor/` tree is no longer imported; the old vendored DS bundle + `vendor/cdn/` assets are dead and may be removed. Witness the live GUI by confirming the import map resolves to unpkg and `.agentchat` renders with 0 console errors.
|
|
151
153
|
|
|
152
154
|
## Agent/model/session management (2026-05-28)
|
|
153
155
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
## [Unreleased] -
|
|
1
|
+
## [Unreleased] - GUI moved into the anentrypoint-design kit, loaded from unpkg
|
|
2
|
+
|
|
3
|
+
- the chat GUI now lives in the design kit: added a reusable `AgentChat` component to anentrypoint-design (agent-then-model picker + AICat auto-scroll/thinking + tool parts + cwd bar + caret-stable composer), pure props-in/vnode-out with all transport as host callbacks. Published via the kit's CI to npm -> unpkg.
|
|
4
|
+
- site/app/index.html: imports anentrypoint-design from https://unpkg.com/anentrypoint-design@latest at runtime (always-latest), replacing the vendored bundle. Trades the offline guarantee for always-latest, per project decision.
|
|
5
|
+
- site/app/js/app.js: chatMain renders the AgentChat kit and wires agentgui state + WS/ccsniff glue as callbacks; the agent/model picker, cwd bar, and composer moved out of agentgui into the kit. backend.js (WS client) stays as the transport glue.
|
|
6
|
+
- verified end-to-end in a real browser: GUI sourced from unpkg, AgentChat renders, chat round-trips (reply streams into the kit's transcript), 0 console errors.
|
|
7
|
+
|
|
8
|
+
## [Unreleased] - live e2e chat verified + composer caret fix + ccsniff memory cap
|
|
9
|
+
|
|
10
|
+
- verified the full GUI chat round-trip end-to-end in a real browser: compose -> WS chat.sendMessage -> claude-code -> streaming_progress -> transcript renders the streamed reply (not just the runner harness).
|
|
11
|
+
- site/app/js/app.js: composer onInput re-renders only on the empty<->non-empty draft transition (the only change that toggles the send button), not on every keystroke. Why: a synchronous re-render per character fought the caret on the controlled textarea.
|
|
12
|
+
- vendored anentrypoint-design DS rebuilt: ChatComposer reads the live textarea value on send and syncs the controlled value into the DOM only when it differs, so a mid-type re-render no longer resets the caret or drops keystrokes. Why: DS override belongs in the kit; the composer is shared.
|
|
13
|
+
- ccsniff (sibling repo, pushed): bounded the in-memory history store (event cap + eviction + per-event text cap) and bounded loadOnce to read newest events first up to the cap. Why: the store grew unbounded to 6GB+ (OOM) and parsed the entire JSONL backlog at load (~2.4GB transient peak); heap now settles ~47MB and load peak ~570MB.
|
|
2
14
|
|
|
3
15
|
- lib/agent-discovery.js + lib/claude-runner-agents.js + lib/agent-descriptors.js: added `agy` (Antigravity) as a direct-protocol CLI agent — `agy --print "<prompt>" --dangerously-skip-permissions [--continue]`, plain-text parseOutput, no model picker. Completes the four flagship orchestration targets (Claude Code, OpenCode, Kilo, Antigravity). Why: the GUI could not drive Antigravity at all.
|
|
4
16
|
- lib/claude-runner.js + lib/claude-runner-direct.js: SECURITY — the direct runner now spawns a resolved binary with `shell:false`. Previously `shell:true` on Windows concatenated argv unescaped, so a prompt containing `&`/`|`/`>`/backticks executed as shell commands (verified injection). Why: arbitrary command execution from chat input.
|
package/package.json
CHANGED
package/site/app/index.html
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
6
|
<title>agentgui</title>
|
|
7
7
|
<meta name="description" content="agentgui — multi-agent client with same-origin server, in-process ccsniff history, and ACP chat.">
|
|
8
|
-
|
|
8
|
+
<!-- The GUI lives in the anentrypoint-design kit and is loaded from unpkg at
|
|
9
|
+
its latest published version, so agentgui always tracks the kit without a
|
|
10
|
+
hand-maintained vendored copy. (This trades the prior offline guarantee for
|
|
11
|
+
always-latest, per project decision; a network connection is required.) -->
|
|
12
|
+
<link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@latest/dist/247420.css">
|
|
9
13
|
<script type="importmap">
|
|
10
|
-
{ "imports": { "anentrypoint-design": "
|
|
14
|
+
{ "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@latest/dist/247420.js" } }
|
|
11
15
|
</script>
|
|
12
16
|
<style>
|
|
13
17
|
:root {
|
package/site/app/js/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as B from './backend.js';
|
|
|
3
3
|
|
|
4
4
|
installStyles().catch(() => {});
|
|
5
5
|
|
|
6
|
-
const { AppShell, Topbar, Crumb, Side, Status, Chat, ChatComposer, Row, Panel, PageHeader, SearchInput, TextField, Select, Btn, EventList, Spinner, Alert } = C;
|
|
6
|
+
const { AppShell, Topbar, Crumb, Side, Status, Chat, ChatComposer, AgentChat, Row, Panel, PageHeader, SearchInput, TextField, Select, Btn, EventList, Spinner, Alert } = C;
|
|
7
7
|
|
|
8
8
|
const state = {
|
|
9
9
|
backend: B.getBackend(),
|
|
@@ -135,6 +135,27 @@ function selectModel(id) {
|
|
|
135
135
|
function agentById(id) { return state.agents.find(a => a.id === id); }
|
|
136
136
|
function agentAvailable(id) { const a = agentById(id); return !a || a.available !== false; }
|
|
137
137
|
|
|
138
|
+
// The four flagship orchestration targets surface first, then other available
|
|
139
|
+
// agents, then npx-installable, then not-installed — so the agents the GUI
|
|
140
|
+
// exists to drive are reachable without scanning a flat 17-item list. This
|
|
141
|
+
// ordering is agentgui's orchestration priority, so it stays in the host and is
|
|
142
|
+
// passed pre-sorted to the (app-agnostic) AgentChat kit.
|
|
143
|
+
const PRIMARY_AGENTS = ['claude-code', 'opencode', 'kilo', 'agy'];
|
|
144
|
+
function sortedAgents() {
|
|
145
|
+
const rank = (a) => {
|
|
146
|
+
const primary = PRIMARY_AGENTS.indexOf(a.id);
|
|
147
|
+
const avail = a.available !== false;
|
|
148
|
+
if (primary !== -1 && avail) return primary;
|
|
149
|
+
if (avail) return 10;
|
|
150
|
+
if (a.npxInstallable) return 20;
|
|
151
|
+
return 30;
|
|
152
|
+
};
|
|
153
|
+
return state.agents
|
|
154
|
+
.map(a => ({ a, rank: rank(a) }))
|
|
155
|
+
.sort((x, y) => x.rank - y.rank || x.a.name.localeCompare(y.a.name))
|
|
156
|
+
.map(({ a }) => a);
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
function navTo(tab) {
|
|
139
160
|
const prev = state.tab;
|
|
140
161
|
state.tab = tab;
|
|
@@ -287,55 +308,9 @@ function view() {
|
|
|
287
308
|
onNav: (label) => navTo(label),
|
|
288
309
|
});
|
|
289
310
|
|
|
290
|
-
// The
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
// the DS Select renders a plain option list.
|
|
294
|
-
const PRIMARY_AGENTS = ['claude-code', 'opencode', 'kilo', 'agy'];
|
|
295
|
-
const agentRank = (a) => {
|
|
296
|
-
const primary = PRIMARY_AGENTS.indexOf(a.id);
|
|
297
|
-
const avail = a.available !== false;
|
|
298
|
-
if (primary !== -1 && avail) return primary; // 0..3 flagship + available
|
|
299
|
-
if (avail) return 10; // other available
|
|
300
|
-
if (a.npxInstallable) return 20; // installable via npx
|
|
301
|
-
return 30; // not installed
|
|
302
|
-
};
|
|
303
|
-
const agentOptions = state.agents
|
|
304
|
-
.map(a => ({ a, rank: agentRank(a) }))
|
|
305
|
-
.sort((x, y) => x.rank - y.rank || x.a.name.localeCompare(y.a.name))
|
|
306
|
-
.map(({ a }) => ({
|
|
307
|
-
value: a.id,
|
|
308
|
-
label: a.name + (a.available === false ? (a.npxInstallable ? ' (via npx)' : ' (not installed)') : ''),
|
|
309
|
-
disabled: a.available === false && !a.npxInstallable,
|
|
310
|
-
}));
|
|
311
|
-
const showModelPicker = state.tab === 'chat' && state.agentModels.length > 0;
|
|
312
|
-
|
|
313
|
-
const crumbRight = state.tab === 'chat'
|
|
314
|
-
? [h('div', { key: 'cc', class: 'chat-controls' },
|
|
315
|
-
Select({
|
|
316
|
-
key: 'agentsel',
|
|
317
|
-
value: state.selectedAgent,
|
|
318
|
-
placeholder: '— agent —',
|
|
319
|
-
title: 'Select coding agent',
|
|
320
|
-
options: agentOptions,
|
|
321
|
-
onChange: (v) => { selectAgent(v); },
|
|
322
|
-
}),
|
|
323
|
-
showModelPicker
|
|
324
|
-
? Select({
|
|
325
|
-
key: 'modelsel',
|
|
326
|
-
value: state.selectedModel,
|
|
327
|
-
placeholder: '— model —',
|
|
328
|
-
title: 'Select model for this agent',
|
|
329
|
-
options: state.agentModels.map(m => ({ value: m.id, label: m.name || m.id })),
|
|
330
|
-
onChange: (v) => { selectModel(v); },
|
|
331
|
-
})
|
|
332
|
-
: null,
|
|
333
|
-
state.chat.busy
|
|
334
|
-
? Btn({ key: 'stop', onClick: cancelChat, children: 'stop', title: 'Stop streaming' })
|
|
335
|
-
: Btn({ key: 'new', onClick: newChat, children: '+ new', title: 'Start new chat (clears history)' }),
|
|
336
|
-
dot,
|
|
337
|
-
)]
|
|
338
|
-
: [dot];
|
|
311
|
+
// The agent/model picker + new/stop now live inside the AgentChat kit (chat
|
|
312
|
+
// tab), so the crumb carries only the status dot on every tab.
|
|
313
|
+
const crumbRight = [dot];
|
|
339
314
|
|
|
340
315
|
// Topbar already shows "agentgui / <tab>"; the crumb is reserved for contextual
|
|
341
316
|
// controls (model picker, new/stop, live status) so it doesn't duplicate the path.
|
|
@@ -407,37 +382,11 @@ function errText(e) {
|
|
|
407
382
|
}
|
|
408
383
|
|
|
409
384
|
function chatMain() {
|
|
410
|
-
const lastIdx = state.chat.messages.length - 1;
|
|
411
385
|
const agentName = agentById(state.selectedAgent)?.name || state.selectedAgent || 'agent';
|
|
412
|
-
const msgs = state.chat.messages.map((m, i) => {
|
|
413
|
-
const isAssistant = m.role === 'assistant';
|
|
414
|
-
const isStreaming = state.chat.busy && i === lastIdx && isAssistant;
|
|
415
|
-
const hasParts = Array.isArray(m.parts) && m.parts.length > 0;
|
|
416
|
-
const isEmptyStreaming = isStreaming && !m.content && !hasParts;
|
|
417
|
-
const parts = [];
|
|
418
|
-
if (m.content) parts.push({ kind: isAssistant ? 'md' : 'text', text: m.content });
|
|
419
|
-
if (hasParts) for (const p of m.parts) parts.push({ kind: 'text', text: p });
|
|
420
|
-
return {
|
|
421
|
-
key: m.id || String(i),
|
|
422
|
-
who: isAssistant ? 'them' : 'you',
|
|
423
|
-
name: isAssistant ? agentName : 'you',
|
|
424
|
-
time: m.time || '',
|
|
425
|
-
typing: isEmptyStreaming,
|
|
426
|
-
parts: isEmptyStreaming ? undefined : (parts.length ? parts : [{ kind: 'text', text: '' }]),
|
|
427
|
-
};
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
const placeholder = !state.selectedAgent
|
|
431
|
-
? 'choose an agent first'
|
|
432
|
-
: (!agentAvailable(state.selectedAgent) ? agentName + ' is not installed' : 'message…');
|
|
433
|
-
const composer = ChatComposer({
|
|
434
|
-
value: state.chat.draft,
|
|
435
|
-
disabled: !canSend(),
|
|
436
|
-
placeholder,
|
|
437
|
-
onInput: (v) => { state.chat.draft = v; render(); },
|
|
438
|
-
onSend: (v) => { state.chat.draft = v; sendChat(); },
|
|
439
|
-
});
|
|
440
386
|
|
|
387
|
+
// Banners agentgui owns (resume, agent-switched, unavailable, confirm-clear,
|
|
388
|
+
// stream error) are pre-built here and handed to the kit, which renders them
|
|
389
|
+
// above the thread. The kit holds no agentgui-specific banner logic.
|
|
441
390
|
const banners = [];
|
|
442
391
|
if (state.chat.resumeSid) {
|
|
443
392
|
banners.push(h('div', { key: 'rb', class: 'resume-banner', role: 'status' },
|
|
@@ -447,7 +396,6 @@ function chatMain() {
|
|
|
447
396
|
banners.push(Alert({ key: 'rnote', kind: 'info', title: 'Agent switched', children: state.chat.resumeNote }));
|
|
448
397
|
}
|
|
449
398
|
}
|
|
450
|
-
banners.push(cwdBanner());
|
|
451
399
|
if (state.selectedAgent && !agentAvailable(state.selectedAgent)) {
|
|
452
400
|
banners.push(Alert({ key: 'unavail', kind: 'warn', title: agentName + ' is not installed',
|
|
453
401
|
children: 'This agent\'s CLI was not found on the server. Pick another agent or install it.' }));
|
|
@@ -459,26 +407,59 @@ function chatMain() {
|
|
|
459
407
|
Btn({ key: 'cnyes', danger: true, onClick: newChat, children: 'clear' }),
|
|
460
408
|
Btn({ key: 'cnno', onClick: () => { state.confirmingNewChat = false; render(); }, children: 'cancel' })] }));
|
|
461
409
|
}
|
|
462
|
-
// Last stream error surfaced as a proper Alert instead of raw JSON in the bubble.
|
|
463
410
|
const lastErr = state.chat.messages.length ? state.chat.messages[state.chat.messages.length - 1].error : null;
|
|
464
411
|
if (lastErr && !state.chat.busy) {
|
|
465
412
|
banners.push(Alert({ key: 'chaterr', kind: 'error', title: 'Stream error', children: lastErr }));
|
|
466
413
|
}
|
|
414
|
+
|
|
415
|
+
const placeholder = !state.selectedAgent
|
|
416
|
+
? 'choose an agent first'
|
|
417
|
+
: (!agentAvailable(state.selectedAgent) ? agentName + ' is not installed' : 'message…');
|
|
418
|
+
|
|
419
|
+
// The reusable AgentChat kit owns the agent/model picker, cwd bar, transcript
|
|
420
|
+
// (with AICat-style auto-scroll + thinking), and the caret-stable composer.
|
|
421
|
+
// agentgui supplies state and wires every server interaction as a callback.
|
|
467
422
|
return [
|
|
468
423
|
offlineBanner(),
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
424
|
+
AgentChat({
|
|
425
|
+
agents: sortedAgents(),
|
|
426
|
+
selectedAgent: state.selectedAgent,
|
|
427
|
+
models: state.agentModels,
|
|
428
|
+
selectedModel: state.selectedModel,
|
|
429
|
+
messages: state.chat.messages,
|
|
430
|
+
busy: state.chat.busy,
|
|
431
|
+
draft: state.chat.draft,
|
|
432
|
+
status: state.chat.busy ? 'streaming…' : (state.chat.resumeSid ? 'resume' : 'ready'),
|
|
433
|
+
agentName,
|
|
434
|
+
placeholder,
|
|
435
|
+
canSend: canSend(),
|
|
436
|
+
banners,
|
|
437
|
+
cwd: state.chatCwd,
|
|
438
|
+
cwdEditing: !!state.cwdEditing,
|
|
439
|
+
cwdDraft: state.cwdDraft,
|
|
440
|
+
onSelectAgent: (v) => selectAgent(v),
|
|
441
|
+
onSelectModel: (v) => selectModel(v),
|
|
442
|
+
onNewChat: newChat,
|
|
443
|
+
onStop: cancelChat,
|
|
444
|
+
onInput: (v) => {
|
|
445
|
+
// The kit's textarea is controlled and reads its live DOM value on send,
|
|
446
|
+
// so re-render only when the draft crosses empty<->non-empty (the only
|
|
447
|
+
// transition that toggles the send button's disabled state).
|
|
448
|
+
const was = !!(state.chat.draft && state.chat.draft.trim());
|
|
449
|
+
const now = !!(v && v.trim());
|
|
450
|
+
state.chat.draft = v;
|
|
451
|
+
if (was !== now) render();
|
|
452
|
+
},
|
|
453
|
+
onSend: (v) => { state.chat.draft = v; sendChat(); },
|
|
454
|
+
onCwdEdit: () => { state.cwdEditing = true; state.cwdDraft = state.chatCwd || ''; render(); },
|
|
455
|
+
onCwdSave: () => {
|
|
456
|
+
state.chatCwd = (state.cwdDraft ?? '').trim();
|
|
457
|
+
if (state.chatCwd) lsSet('agentgui.cwd', state.chatCwd); else lsRemove('agentgui.cwd');
|
|
458
|
+
state.cwdEditing = false; state.cwdDraft = undefined; render();
|
|
459
|
+
},
|
|
460
|
+
onCwdCancel: () => { state.cwdEditing = false; state.cwdDraft = undefined; render(); },
|
|
461
|
+
onCwdClear: () => { state.chatCwd = ''; lsRemove('agentgui.cwd'); render(); },
|
|
462
|
+
onCwdDraft: (v) => { state.cwdDraft = v; },
|
|
482
463
|
}),
|
|
483
464
|
].filter(Boolean);
|
|
484
465
|
}
|
|
@@ -489,24 +470,8 @@ function offlineBanner() {
|
|
|
489
470
|
children: 'agentgui can\'t reach the server (' + (state.health.error || state.health.status) + '). Chat and history actions will fail until it reconnects.' });
|
|
490
471
|
}
|
|
491
472
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return h('div', { key: 'cwdb', class: 'resume-banner', role: 'group', 'aria-label': 'Set working directory' },
|
|
495
|
-
TextField({ key: 'cwdfield', label: 'working directory (blank = server default)', value: state.cwdDraft ?? state.chatCwd ?? '',
|
|
496
|
-
placeholder: 'absolute path', onInput: (v) => { state.cwdDraft = v; } }),
|
|
497
|
-
Btn({ key: 'cwdsave', primary: true, onClick: () => {
|
|
498
|
-
state.chatCwd = (state.cwdDraft ?? '').trim();
|
|
499
|
-
if (state.chatCwd) lsSet('agentgui.cwd', state.chatCwd); else lsRemove('agentgui.cwd');
|
|
500
|
-
state.cwdEditing = false; state.cwdDraft = undefined; render();
|
|
501
|
-
}, children: 'save' }),
|
|
502
|
-
Btn({ key: 'cwdcancel', onClick: () => { state.cwdEditing = false; state.cwdDraft = undefined; render(); }, children: 'cancel' }));
|
|
503
|
-
}
|
|
504
|
-
return h('div', { key: 'cwdb', class: 'cwd-bar', role: 'group', 'aria-label': 'Working directory' },
|
|
505
|
-
h('span', { key: 'cwdtxt', class: 'lede cwd-bar-text', title: state.chatCwd || 'server default working directory' },
|
|
506
|
-
state.chatCwd ? 'cwd: ' + truncate(state.chatCwd, 28, 60) : 'cwd: server default'),
|
|
507
|
-
h('button', { key: 'cwdset', type: 'button', class: 'cwd-bar-btn', onClick: () => { state.cwdEditing = true; state.cwdDraft = state.chatCwd || ''; render(); } }, state.chatCwd ? 'change' : 'set'),
|
|
508
|
-
state.chatCwd ? h('button', { key: 'cwdclr', type: 'button', class: 'cwd-bar-btn', onClick: () => { state.chatCwd = ''; lsRemove('agentgui.cwd'); render(); } }, 'use default') : null);
|
|
509
|
-
}
|
|
473
|
+
// (The working-directory bar now lives in the AgentChat kit; agentgui wires its
|
|
474
|
+
// cwd state + handlers as kit callbacks in chatMain.)
|
|
510
475
|
|
|
511
476
|
function newChat() {
|
|
512
477
|
if (state.chat.messages.length && !state.confirmingNewChat) {
|
|
@@ -518,14 +518,43 @@
|
|
|
518
518
|
|
|
519
519
|
.ds-247420 .app-topbar {
|
|
520
520
|
position: sticky; top: 0; z-index: var(--z-header);
|
|
521
|
-
display:
|
|
521
|
+
display: flex; flex-wrap: wrap;
|
|
522
522
|
align-items: center; gap: var(--space-4);
|
|
523
523
|
min-height: var(--app-topbar-h);
|
|
524
524
|
padding: 10px var(--pad-x);
|
|
525
525
|
background: color-mix(in oklab, var(--bg) 88%, transparent);
|
|
526
526
|
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
|
|
527
527
|
}
|
|
528
|
-
|
|
528
|
+
|
|
529
|
+
/* Merged chrome: when AppShell gets both a topbar and a crumb it wraps them
|
|
530
|
+
in .app-chrome and they share ONE sticky band instead of stacking as two
|
|
531
|
+
bars. The breadcrumb provides the left identity (it already begins with the
|
|
532
|
+
brand), so the topbar's standalone brand is hidden to avoid showing the name
|
|
533
|
+
twice; nav and the crumb's right slot sit together on the right. */
|
|
534
|
+
.ds-247420 .app-chrome {
|
|
535
|
+
position: sticky; top: 0; z-index: var(--z-header);
|
|
536
|
+
display: flex; flex-wrap: wrap; align-items: center;
|
|
537
|
+
gap: var(--space-2) var(--space-4);
|
|
538
|
+
padding: 8px var(--pad-x);
|
|
539
|
+
background: color-mix(in oklab, var(--bg) 88%, transparent);
|
|
540
|
+
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
|
|
541
|
+
}
|
|
542
|
+
.ds-247420 .app-chrome > .app-topbar,
|
|
543
|
+
.ds-247420 .app-chrome > .app-crumb {
|
|
544
|
+
position: static; background: none; backdrop-filter: none;
|
|
545
|
+
-webkit-backdrop-filter: none; padding: 0; min-height: 0;
|
|
546
|
+
flex: 1 1 auto;
|
|
547
|
+
}
|
|
548
|
+
.ds-247420 .app-chrome > .app-crumb { order: 1; flex: 1 1 auto; }
|
|
549
|
+
.ds-247420 .app-chrome > .app-topbar { order: 2; flex: 0 0 auto; }
|
|
550
|
+
.ds-247420 .app-chrome > .app-topbar > .brand { display: none; }
|
|
551
|
+
.ds-247420 .app-chrome > .app-topbar > nav { margin-left: 0; }
|
|
552
|
+
.ds-247420 .app-chrome > .app-crumb > .crumb-right { margin-left: auto; }
|
|
553
|
+
.ds-247420 .app-topbar > .brand { flex: 0 0 auto; }
|
|
554
|
+
.ds-247420 .app-topbar > .app-search { flex: 1 1 auto; }
|
|
555
|
+
.ds-247420 .app-topbar > nav { margin-left: auto; }
|
|
556
|
+
.ds-247420 .app-topbar nav { display: flex; gap: 4px; font-size: var(--fs-sm); flex-wrap: wrap; }
|
|
557
|
+
.ds-247420 .app-topbar nav a { flex: 0 0 auto; }
|
|
529
558
|
.ds-247420 .app-topbar nav a {
|
|
530
559
|
color: var(--fg-2);
|
|
531
560
|
padding: 12px 14px;
|
|
@@ -1083,25 +1112,21 @@
|
|
|
1083
1112
|
/* App Layout: single-column + drawer is handled once in the ≤900px block;
|
|
1084
1113
|
no need to re-declare grid-template-columns here (was a redundant !important). */
|
|
1085
1114
|
|
|
1086
|
-
/* Topbar Navigation
|
|
1115
|
+
/* Topbar Navigation: stack the nav onto its own full-width row below the
|
|
1116
|
+
brand and let it scroll horizontally. The brand stays on the first row;
|
|
1117
|
+
nav wraps to a second row at 100% width (flex-wrap on .app-topbar). */
|
|
1087
1118
|
.ds-247420 .app-topbar {
|
|
1088
|
-
grid-template-columns: 1fr auto;
|
|
1089
1119
|
gap: var(--space-2); padding: 12px var(--space-3);
|
|
1090
1120
|
}
|
|
1091
|
-
.ds-247420 .app-topbar nav
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
}
|
|
1095
|
-
/* Keep primary nav reachable on small screens (apps without a sidebar have
|
|
1096
|
-
no other entry point). Compact it and let it scroll horizontally rather
|
|
1097
|
-
than hiding it entirely. */
|
|
1098
|
-
.ds-247420 .app-topbar nav {
|
|
1099
|
-
display: flex; gap: 2px;
|
|
1121
|
+
.ds-247420 .app-topbar > nav {
|
|
1122
|
+
flex: 1 1 100%; margin-left: 0; order: 3;
|
|
1123
|
+
display: flex; gap: 2px; flex-wrap: nowrap;
|
|
1100
1124
|
overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
|
|
1101
|
-
max-width: 60vw;
|
|
1102
1125
|
}
|
|
1103
1126
|
.ds-247420 .app-topbar nav::-webkit-scrollbar { display: none; }
|
|
1104
|
-
.ds-247420 .app-topbar nav a {
|
|
1127
|
+
.ds-247420 .app-topbar nav a {
|
|
1128
|
+
flex: 0 0 auto; padding: 10px 12px; min-height: 44px; white-space: nowrap;
|
|
1129
|
+
}
|
|
1105
1130
|
.ds-247420 .brand { font-size: var(--fs-tiny); font-weight: 600; }
|
|
1106
1131
|
|
|
1107
1132
|
/* Search */
|