anentrypoint-design 0.0.424 → 0.0.426
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/app-shell.css +5 -0
- package/chat.css +41 -0
- package/colors_and_type.css +86 -42
- package/dist/247420.css +544 -82
- package/dist/247420.js +34 -28
- package/package.json +1 -1
- package/src/components/calendar/calendar.js +144 -0
- package/src/components/calendar/date-picker.js +106 -0
- package/src/components/calendar/grid.js +79 -0
- package/src/components/calendar.js +15 -0
- package/src/components/carousel.js +53 -0
- package/src/components/collab/cursors.js +68 -0
- package/src/components/collab/presence.js +29 -0
- package/src/components/collab.js +11 -0
- package/src/components/content/otp-input.js +91 -0
- package/src/components/content.js +2 -1
- package/src/components/context-pane/meter.js +42 -0
- package/src/components/context-pane/pane.js +127 -0
- package/src/components/context-pane/treemap.js +81 -0
- package/src/components/context-pane/xray.js +30 -0
- package/src/components/context-pane.js +12 -127
- package/src/components/data-density/progress.js +20 -0
- package/src/components/data-density.js +3 -0
- package/src/components/editor-primitives/layout.js +12 -0
- package/src/components/editor-primitives.js +2 -2
- package/src/components/overlay-primitives/hover-card.js +60 -0
- package/src/components/overlay-primitives/menubar.js +65 -0
- package/src/components/overlay-primitives.js +4 -0
- package/src/components/shell/icons.js +4 -1
- package/src/components/slider.js +66 -0
- package/src/components.js +21 -5
- package/src/css/app-shell/base.css +16 -0
- package/src/css/app-shell/calendar.css +131 -0
- package/src/css/app-shell/carousel.css +44 -0
- package/src/css/app-shell/chat-polish.css +1 -1
- package/src/css/app-shell/collab.css +106 -0
- package/src/css/app-shell/hero-content.css +34 -38
- package/src/css/app-shell/kits-appended.css +1 -1
- package/src/css/app-shell/otp-input.css +31 -0
- package/src/css/app-shell/slider.css +53 -0
- package/src/page-html/page-styles.js +7 -1
- package/types/components.d.ts +299 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// ContextMeter — horizontal meter bar showing proportional colored segments
|
|
2
|
+
// of token usage (e.g. system/user/assistant) against a total, plus a
|
|
3
|
+
// remaining-capacity sliver. Token-styled: each segment's color is a CSS
|
|
4
|
+
// custom property set inline (the one runtime-value exception the inline-
|
|
5
|
+
// style ban already carves out for percentage widths elsewhere in this kit,
|
|
6
|
+
// e.g. BarRow/BarChart's --bar-pct), the palette itself lives in
|
|
7
|
+
// context-pane.css as semantic classes, never a raw hex per segment.
|
|
8
|
+
|
|
9
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
10
|
+
const h = webjsx.createElement;
|
|
11
|
+
|
|
12
|
+
// ContextMeter({ used, total, segments })
|
|
13
|
+
// used : tokens consumed so far (number).
|
|
14
|
+
// total : total budget (number) — the meter's 100%.
|
|
15
|
+
// segments : [{ label, value, tone }] — tone is a semantic class suffix
|
|
16
|
+
// ('system'|'user'|'assistant'|'other'); values need not sum to
|
|
17
|
+
// `used` exactly but are clamped to the remaining budget.
|
|
18
|
+
export function ContextMeter({ used = 0, total = 0, segments = [] } = {}) {
|
|
19
|
+
const safeTotal = total > 0 ? total : 1;
|
|
20
|
+
let acc = 0;
|
|
21
|
+
const bars = segments.map((seg, i) => {
|
|
22
|
+
const val = Math.max(0, Number(seg.value) || 0);
|
|
23
|
+
const pct = Math.max(0, Math.min(100, (val / safeTotal) * 100));
|
|
24
|
+
acc += val;
|
|
25
|
+
return h('span', {
|
|
26
|
+
key: seg.id || i,
|
|
27
|
+
class: 'ds-context-meter-seg ds-context-meter-seg-' + (seg.tone || 'other'),
|
|
28
|
+
style: `width:${pct}%`,
|
|
29
|
+
title: `${seg.label || 'segment'}: ${val}`,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
const usedPct = Math.max(0, Math.min(100, (used / safeTotal) * 100));
|
|
33
|
+
return h('div', { class: 'ds-context-meter' },
|
|
34
|
+
h('div', {
|
|
35
|
+
class: 'ds-context-meter-track', role: 'meter',
|
|
36
|
+
'aria-label': 'context usage', 'aria-valuenow': String(used),
|
|
37
|
+
'aria-valuemin': '0', 'aria-valuemax': String(total),
|
|
38
|
+
}, ...bars),
|
|
39
|
+
h('div', { class: 'ds-context-meter-foot' },
|
|
40
|
+
h('span', {}, `${used.toLocaleString()} / ${total.toLocaleString()} tokens`),
|
|
41
|
+
h('span', {}, `${Math.round(usedPct)}%`)));
|
|
42
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// ContextPane — a compact right-hand context panel for the chat surface.
|
|
2
|
+
//
|
|
3
|
+
// Surfaces the current conversation's agent, model, working directory, and a
|
|
4
|
+
// live count of running tool calls in the in-flight turn. Built from the kit's
|
|
5
|
+
// Panel + Row primitives so it inherits the design tokens and rail semantics.
|
|
6
|
+
//
|
|
7
|
+
// Usage (consumer wires its own state):
|
|
8
|
+
// ContextPane({ agent, model, cwd, toolCount, onSetCwd })
|
|
9
|
+
//
|
|
10
|
+
// Props:
|
|
11
|
+
// agent : display name of the active agent (string) or falsy for "none"
|
|
12
|
+
// model : model id/name (string) or falsy
|
|
13
|
+
// cwd : the chat working directory (string) or falsy for server default
|
|
14
|
+
// toolCount : number of tool calls running in the current live turn (>=0)
|
|
15
|
+
// usage : OPTIONAL last-turn usage { inputTokens, outputTokens, costUsd, turns, durationMs }
|
|
16
|
+
// session : OPTIONAL whole-conversation totals { turns, cost } shown as a block
|
|
17
|
+
// recentFiles: OPTIONAL [{ path, time }] files touched by tool calls this
|
|
18
|
+
// session (most-recent first), rendered as a compact panel -
|
|
19
|
+
// Claude Desktop's context surfaces recently-touched files.
|
|
20
|
+
// onSetCwd : optional callback for the "set working directory" affordance
|
|
21
|
+
// onOpenFile : optional callback(path) for clicking a recent-files row
|
|
22
|
+
//
|
|
23
|
+
// No decorative glyphs — words + the kit's Icon SVGs only.
|
|
24
|
+
|
|
25
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
26
|
+
import { Panel, Row } from '../content.js';
|
|
27
|
+
import { Btn } from '../shell.js';
|
|
28
|
+
import { fmtDuration } from '../sessions.js';
|
|
29
|
+
|
|
30
|
+
const h = webjsx.createElement;
|
|
31
|
+
|
|
32
|
+
function fmtTok(n) {
|
|
33
|
+
if (n == null) return null;
|
|
34
|
+
if (n < 1000) return String(n);
|
|
35
|
+
if (n < 1000000) return (n / 1000).toFixed(n < 10000 ? 1 : 0) + 'k';
|
|
36
|
+
return (n / 1000000).toFixed(1) + 'M';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ContextPane({ agent, model, cwd, toolCount = 0, usage, session, recentFiles, onSetCwd, onOpenFile } = {}) {
|
|
40
|
+
const running = Number(toolCount) > 0;
|
|
41
|
+
const hasUsage = usage && (usage.inputTokens != null || usage.outputTokens != null || usage.costUsd != null);
|
|
42
|
+
const hasSession = session && (session.turns != null || session.cost != null);
|
|
43
|
+
// Empty state: before an agent is picked AND with no usage/session, four
|
|
44
|
+
// placeholder rows (agent: none / model: dash / ...) read as a dead panel.
|
|
45
|
+
// Show one honest line instead.
|
|
46
|
+
if (!agent && !hasUsage && !hasSession && !cwd) {
|
|
47
|
+
return h('div', { class: 'ds-context' },
|
|
48
|
+
h('div', { class: 'ds-context-empty', role: 'status' },
|
|
49
|
+
'No active conversation — start a chat to see context here'),
|
|
50
|
+
onSetCwd ? h('div', { class: 'ds-context-actions' }, Btn({ onClick: onSetCwd, children: 'set working dir' })) : null);
|
|
51
|
+
}
|
|
52
|
+
// Each Panel's children array is all-unkeyed (no key prop on any sibling),
|
|
53
|
+
// so webjsx never sees a mixed keyed/unkeyed array here.
|
|
54
|
+
const panels = [
|
|
55
|
+
Panel({
|
|
56
|
+
title: 'context',
|
|
57
|
+
children: [
|
|
58
|
+
Row({ title: 'agent', meta: agent || 'none' }),
|
|
59
|
+
Row({ title: 'model', meta: model || '—' }),
|
|
60
|
+
// Wrapped in a scoping div (`ds-context-cwd-row`) so the cwd
|
|
61
|
+
// fact's `.sub` text can be styled monospace to match
|
|
62
|
+
// .ds-dash-cwd/.ds-session-agent/.ds-dash-model without
|
|
63
|
+
// affecting every other Row's `.sub` in the app.
|
|
64
|
+
h('div', { class: 'ds-context-cwd-row' }, Row({
|
|
65
|
+
title: 'working dir',
|
|
66
|
+
sub: cwd || 'server default',
|
|
67
|
+
// Use the rail tone consistently with the GUI-wide semantics:
|
|
68
|
+
// green = active/ok. A default cwd carries no rail (neutral).
|
|
69
|
+
rail: cwd ? 'green' : null,
|
|
70
|
+
// The change-cwd affordance belongs ON the working-dir fact,
|
|
71
|
+
// not as a button floating under the panels.
|
|
72
|
+
onClick: onSetCwd || undefined,
|
|
73
|
+
meta: onSetCwd ? 'change' : undefined,
|
|
74
|
+
})),
|
|
75
|
+
Row({
|
|
76
|
+
title: 'running tools',
|
|
77
|
+
meta: running ? String(toolCount) : 'idle',
|
|
78
|
+
rail: running ? 'purple' : null,
|
|
79
|
+
}),
|
|
80
|
+
],
|
|
81
|
+
}),
|
|
82
|
+
];
|
|
83
|
+
// Conversation block: whole-session totals (turn count + accumulated cost)
|
|
84
|
+
// between the context panel and the per-turn usage panel. All-zero totals
|
|
85
|
+
// are noise, not context - hide the block until there is a conversation.
|
|
86
|
+
// Rendered as a lighter fact group (no card chrome) since it's 1-2 short
|
|
87
|
+
// facts, not enough weight to justify a full bordered Panel.
|
|
88
|
+
if (hasSession && (Number(session.turns) > 0 || Number(session.cost) > 0)) {
|
|
89
|
+
const sesRows = [];
|
|
90
|
+
if (session.turns != null) sesRows.push(Row({ title: 'turns', meta: String(session.turns) }));
|
|
91
|
+
if (session.cost != null) sesRows.push(Row({ title: 'total cost', meta: '$' + Number(session.cost).toFixed(4) }));
|
|
92
|
+
panels.push(h('div', { class: 'ds-context-group' },
|
|
93
|
+
h('div', { class: 'ds-context-group-label' }, 'conversation'),
|
|
94
|
+
...sesRows));
|
|
95
|
+
}
|
|
96
|
+
// Usage block: surface the last turn's token/cost/turn/duration so the
|
|
97
|
+
// result event is no longer silently dropped. Lighter fact group, not a
|
|
98
|
+
// full Panel - same reasoning as the conversation block above.
|
|
99
|
+
if (hasUsage) {
|
|
100
|
+
const tokRows = [];
|
|
101
|
+
if (usage.inputTokens != null) tokRows.push(Row({ title: 'input', meta: fmtTok(usage.inputTokens) + ' tok' }));
|
|
102
|
+
if (usage.outputTokens != null) tokRows.push(Row({ title: 'output', meta: fmtTok(usage.outputTokens) + ' tok' }));
|
|
103
|
+
if (usage.costUsd != null) tokRows.push(Row({ title: 'cost', meta: '$' + usage.costUsd.toFixed(4) }));
|
|
104
|
+
if (usage.turns != null) tokRows.push(Row({ title: 'turns', meta: String(usage.turns) }));
|
|
105
|
+
// One duration vocabulary kit-wide: shared fmtDuration (s -> m -> h).
|
|
106
|
+
if (usage.durationMs != null) tokRows.push(Row({ title: 'duration', meta: fmtDuration(usage.durationMs) }));
|
|
107
|
+
panels.push(h('div', { class: 'ds-context-group' },
|
|
108
|
+
h('div', { class: 'ds-context-group-label' }, 'last turn'),
|
|
109
|
+
...tokRows));
|
|
110
|
+
}
|
|
111
|
+
// Recent files: files touched by tool calls this session, most-recent
|
|
112
|
+
// first, capped to 5 rows so the panel stays a glance not a log. Lighter
|
|
113
|
+
// fact group, not a full Panel - same reasoning as above.
|
|
114
|
+
if (Array.isArray(recentFiles) && recentFiles.length) {
|
|
115
|
+
const fileRows = recentFiles.slice(0, 5).map((f) => Row({
|
|
116
|
+
title: f.path.split(/[/\\]/).filter(Boolean).pop() || f.path,
|
|
117
|
+
sub: f.path,
|
|
118
|
+
meta: f.time || undefined,
|
|
119
|
+
onClick: onOpenFile ? () => onOpenFile(f.path) : undefined,
|
|
120
|
+
}));
|
|
121
|
+
panels.push(h('div', { class: 'ds-context-group' },
|
|
122
|
+
h('div', { class: 'ds-context-group-label' }, 'recent files'),
|
|
123
|
+
...fileRows));
|
|
124
|
+
}
|
|
125
|
+
// The cwd action lives on the working-dir row above; no floating footer button.
|
|
126
|
+
return h('div', { class: 'ds-context' }, ...panels);
|
|
127
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// ContextTreemap — squarified-treemap SVG layout for a token/cost breakdown,
|
|
2
|
+
// nested rectangles sized by value. Raw SVG, no charting library — the only
|
|
3
|
+
// reusable precedent in this kit is charts.js's Sparkline, which maps values
|
|
4
|
+
// to a single polyline (no area-layout code to build on), so the squarified
|
|
5
|
+
// algorithm here is self-contained. Token-stroke/fill only, same convention
|
|
6
|
+
// as Sparkline/BarChart in content/charts.js.
|
|
7
|
+
|
|
8
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
9
|
+
const h = webjsx.createElement;
|
|
10
|
+
|
|
11
|
+
// squarify(items, x, y, w, h) -> [{ item, x, y, w, h }]
|
|
12
|
+
// Classic squarified treemap (Bruls/Huizing/Wijk): lays out `items` (each
|
|
13
|
+
// with a numeric `.value`) into the x/y/w/h rect, recursively slicing off
|
|
14
|
+
// rows that keep aspect ratios closest to 1.
|
|
15
|
+
function worstRatio(row, len, totalArea) {
|
|
16
|
+
const sum = row.reduce((s, v) => s + v, 0);
|
|
17
|
+
if (sum === 0) return Infinity;
|
|
18
|
+
const rowLenSq = len * len, sumSq = sum * sum;
|
|
19
|
+
let worst = 0;
|
|
20
|
+
for (const v of row) {
|
|
21
|
+
const r = Math.max((rowLenSq * v) / sumSq, sumSq / (rowLenSq * v));
|
|
22
|
+
if (r > worst) worst = r;
|
|
23
|
+
}
|
|
24
|
+
return worst;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function squarify(items, x, y, w, h) {
|
|
28
|
+
const values = items.map((it) => Math.max(0, Number(it.value) || 0));
|
|
29
|
+
const total = values.reduce((s, v) => s + v, 0);
|
|
30
|
+
if (!items.length || total <= 0) return [];
|
|
31
|
+
const scale = (w * h) / total;
|
|
32
|
+
const scaled = values.map((v) => v * scale);
|
|
33
|
+
const out = [];
|
|
34
|
+
let idx = 0, rx = x, ry = y, rw = w, rh = h;
|
|
35
|
+
while (idx < items.length) {
|
|
36
|
+
const horizontal = rw >= rh;
|
|
37
|
+
const len = horizontal ? rh : rw;
|
|
38
|
+
let row = [scaled[idx]], rowItems = [idx];
|
|
39
|
+
let i = idx + 1;
|
|
40
|
+
while (i < scaled.length) {
|
|
41
|
+
const trial = [...row, scaled[i]];
|
|
42
|
+
if (worstRatio(trial, len, 0) <= worstRatio(row, len, 0)) { row = trial; rowItems.push(i); i++; }
|
|
43
|
+
else break;
|
|
44
|
+
}
|
|
45
|
+
const rowSum = row.reduce((s, v) => s + v, 0);
|
|
46
|
+
const thickness = len > 0 ? rowSum / len : 0;
|
|
47
|
+
let cursor = horizontal ? ry : rx;
|
|
48
|
+
rowItems.forEach((itemIdx, k) => {
|
|
49
|
+
const size = len > 0 ? row[k] / thickness : 0;
|
|
50
|
+
const rect = horizontal
|
|
51
|
+
? { x: rx, y: cursor, w: thickness, h: size }
|
|
52
|
+
: { x: cursor, y: ry, w: size, h: thickness };
|
|
53
|
+
out.push({ item: items[itemIdx], ...rect });
|
|
54
|
+
cursor += size;
|
|
55
|
+
});
|
|
56
|
+
if (horizontal) { rx += thickness; rw -= thickness; }
|
|
57
|
+
else { ry += thickness; rh -= thickness; }
|
|
58
|
+
idx = i;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ContextTreemap({ items, width=280, height=160 })
|
|
64
|
+
// items: [{ id, label, value, tone }] — tone is a semantic class suffix,
|
|
65
|
+
// same vocabulary as ContextMeter's segment tone.
|
|
66
|
+
export function ContextTreemap({ items = [], width = 280, height = 160 } = {}) {
|
|
67
|
+
const rects = squarify(items, 0, 0, width, height);
|
|
68
|
+
if (!rects.length) return h('div', { class: 'ds-context-treemap-empty' }, 'no breakdown yet');
|
|
69
|
+
return h('svg', {
|
|
70
|
+
class: 'ds-context-treemap', viewBox: `0 0 ${width} ${height}`, width, height,
|
|
71
|
+
role: 'img', 'aria-label': 'context breakdown treemap',
|
|
72
|
+
},
|
|
73
|
+
...rects.map((r, i) => h('g', { key: r.item.id || i },
|
|
74
|
+
h('rect', {
|
|
75
|
+
x: r.x.toFixed(1), y: r.y.toFixed(1), width: Math.max(0, r.w - 1).toFixed(1), height: Math.max(0, r.h - 1).toFixed(1),
|
|
76
|
+
class: 'ds-context-treemap-rect ds-context-treemap-rect-' + (r.item.tone || 'other'),
|
|
77
|
+
}),
|
|
78
|
+
r.w > 40 && r.h > 18
|
|
79
|
+
? h('text', { x: (r.x + 4).toFixed(1), y: (r.y + 14).toFixed(1), class: 'ds-context-treemap-label' }, r.item.label)
|
|
80
|
+
: h('title', {}, `${r.item.label}: ${r.item.value}`))));
|
|
81
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// ContextXRayPanel — per-segment expandable/collapsible breakdown list.
|
|
2
|
+
// Reuses editor-primitives/collapse.js's CollapseGroup for the actual
|
|
3
|
+
// expand/collapse behavior (single-open-at-a-time accordion) rather than
|
|
4
|
+
// reimplementing that state machine; this module only supplies the
|
|
5
|
+
// segment-row content (label, value, sub-items) that goes inside each
|
|
6
|
+
// Collapse body.
|
|
7
|
+
|
|
8
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
9
|
+
import { CollapseGroup } from '../editor-primitives/collapse.js';
|
|
10
|
+
const h = webjsx.createElement;
|
|
11
|
+
|
|
12
|
+
// ContextXRayPanel({ segments, openId, onOpenIdChange })
|
|
13
|
+
// segments: [{ id, label, value, tone, items: [{ label, value }] }]
|
|
14
|
+
// openId/onOpenIdChange: accordion state, forwarded straight to CollapseGroup.
|
|
15
|
+
export function ContextXRayPanel({ segments = [], openId, onOpenIdChange } = {}) {
|
|
16
|
+
if (!segments.length) return h('div', { class: 'ds-context-xray-empty' }, 'no segments yet');
|
|
17
|
+
const items = segments.map((seg) => ({
|
|
18
|
+
id: seg.id,
|
|
19
|
+
title: h('span', { class: 'ds-context-xray-head' },
|
|
20
|
+
h('span', { class: 'ds-context-xray-dot ds-context-xray-dot-' + (seg.tone || 'other') }),
|
|
21
|
+
h('span', { class: 'ds-context-xray-label' }, seg.label),
|
|
22
|
+
h('span', { class: 'ds-context-xray-value' }, String(seg.value))),
|
|
23
|
+
children: h('div', { class: 'ds-context-xray-body' },
|
|
24
|
+
...(seg.items || []).map((it, i) => h('div', { key: i, class: 'ds-context-xray-row' },
|
|
25
|
+
h('span', {}, it.label),
|
|
26
|
+
h('span', {}, String(it.value))))),
|
|
27
|
+
}));
|
|
28
|
+
return h('div', { class: 'ds-context-xray' },
|
|
29
|
+
CollapseGroup({ items, openId, onOpenChange: onOpenIdChange, accordion: true }));
|
|
30
|
+
}
|
|
@@ -1,127 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// cwd : the chat working directory (string) or falsy for server default
|
|
14
|
-
// toolCount : number of tool calls running in the current live turn (>=0)
|
|
15
|
-
// usage : OPTIONAL last-turn usage { inputTokens, outputTokens, costUsd, turns, durationMs }
|
|
16
|
-
// session : OPTIONAL whole-conversation totals { turns, cost } shown as a block
|
|
17
|
-
// recentFiles: OPTIONAL [{ path, time }] files touched by tool calls this
|
|
18
|
-
// session (most-recent first), rendered as a compact panel -
|
|
19
|
-
// Claude Desktop's context surfaces recently-touched files.
|
|
20
|
-
// onSetCwd : optional callback for the "set working directory" affordance
|
|
21
|
-
// onOpenFile : optional callback(path) for clicking a recent-files row
|
|
22
|
-
//
|
|
23
|
-
// No decorative glyphs — words + the kit's Icon SVGs only.
|
|
24
|
-
|
|
25
|
-
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
26
|
-
import { Panel, Row } from './content.js';
|
|
27
|
-
import { Btn } from './shell.js';
|
|
28
|
-
import { fmtDuration } from './sessions.js';
|
|
29
|
-
|
|
30
|
-
const h = webjsx.createElement;
|
|
31
|
-
|
|
32
|
-
function fmtTok(n) {
|
|
33
|
-
if (n == null) return null;
|
|
34
|
-
if (n < 1000) return String(n);
|
|
35
|
-
if (n < 1000000) return (n / 1000).toFixed(n < 10000 ? 1 : 0) + 'k';
|
|
36
|
-
return (n / 1000000).toFixed(1) + 'M';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function ContextPane({ agent, model, cwd, toolCount = 0, usage, session, recentFiles, onSetCwd, onOpenFile } = {}) {
|
|
40
|
-
const running = Number(toolCount) > 0;
|
|
41
|
-
const hasUsage = usage && (usage.inputTokens != null || usage.outputTokens != null || usage.costUsd != null);
|
|
42
|
-
const hasSession = session && (session.turns != null || session.cost != null);
|
|
43
|
-
// Empty state: before an agent is picked AND with no usage/session, four
|
|
44
|
-
// placeholder rows (agent: none / model: dash / ...) read as a dead panel.
|
|
45
|
-
// Show one honest line instead.
|
|
46
|
-
if (!agent && !hasUsage && !hasSession && !cwd) {
|
|
47
|
-
return h('div', { class: 'ds-context' },
|
|
48
|
-
h('div', { class: 'ds-context-empty', role: 'status' },
|
|
49
|
-
'No active conversation — start a chat to see context here'),
|
|
50
|
-
onSetCwd ? h('div', { class: 'ds-context-actions' }, Btn({ onClick: onSetCwd, children: 'set working dir' })) : null);
|
|
51
|
-
}
|
|
52
|
-
// Each Panel's children array is all-unkeyed (no key prop on any sibling),
|
|
53
|
-
// so webjsx never sees a mixed keyed/unkeyed array here.
|
|
54
|
-
const panels = [
|
|
55
|
-
Panel({
|
|
56
|
-
title: 'context',
|
|
57
|
-
children: [
|
|
58
|
-
Row({ title: 'agent', meta: agent || 'none' }),
|
|
59
|
-
Row({ title: 'model', meta: model || '—' }),
|
|
60
|
-
// Wrapped in a scoping div (`ds-context-cwd-row`) so the cwd
|
|
61
|
-
// fact's `.sub` text can be styled monospace to match
|
|
62
|
-
// .ds-dash-cwd/.ds-session-agent/.ds-dash-model without
|
|
63
|
-
// affecting every other Row's `.sub` in the app.
|
|
64
|
-
h('div', { class: 'ds-context-cwd-row' }, Row({
|
|
65
|
-
title: 'working dir',
|
|
66
|
-
sub: cwd || 'server default',
|
|
67
|
-
// Use the rail tone consistently with the GUI-wide semantics:
|
|
68
|
-
// green = active/ok. A default cwd carries no rail (neutral).
|
|
69
|
-
rail: cwd ? 'green' : null,
|
|
70
|
-
// The change-cwd affordance belongs ON the working-dir fact,
|
|
71
|
-
// not as a button floating under the panels.
|
|
72
|
-
onClick: onSetCwd || undefined,
|
|
73
|
-
meta: onSetCwd ? 'change' : undefined,
|
|
74
|
-
})),
|
|
75
|
-
Row({
|
|
76
|
-
title: 'running tools',
|
|
77
|
-
meta: running ? String(toolCount) : 'idle',
|
|
78
|
-
rail: running ? 'purple' : null,
|
|
79
|
-
}),
|
|
80
|
-
],
|
|
81
|
-
}),
|
|
82
|
-
];
|
|
83
|
-
// Conversation block: whole-session totals (turn count + accumulated cost)
|
|
84
|
-
// between the context panel and the per-turn usage panel. All-zero totals
|
|
85
|
-
// are noise, not context - hide the block until there is a conversation.
|
|
86
|
-
// Rendered as a lighter fact group (no card chrome) since it's 1-2 short
|
|
87
|
-
// facts, not enough weight to justify a full bordered Panel.
|
|
88
|
-
if (hasSession && (Number(session.turns) > 0 || Number(session.cost) > 0)) {
|
|
89
|
-
const sesRows = [];
|
|
90
|
-
if (session.turns != null) sesRows.push(Row({ title: 'turns', meta: String(session.turns) }));
|
|
91
|
-
if (session.cost != null) sesRows.push(Row({ title: 'total cost', meta: '$' + Number(session.cost).toFixed(4) }));
|
|
92
|
-
panels.push(h('div', { class: 'ds-context-group' },
|
|
93
|
-
h('div', { class: 'ds-context-group-label' }, 'conversation'),
|
|
94
|
-
...sesRows));
|
|
95
|
-
}
|
|
96
|
-
// Usage block: surface the last turn's token/cost/turn/duration so the
|
|
97
|
-
// result event is no longer silently dropped. Lighter fact group, not a
|
|
98
|
-
// full Panel - same reasoning as the conversation block above.
|
|
99
|
-
if (hasUsage) {
|
|
100
|
-
const tokRows = [];
|
|
101
|
-
if (usage.inputTokens != null) tokRows.push(Row({ title: 'input', meta: fmtTok(usage.inputTokens) + ' tok' }));
|
|
102
|
-
if (usage.outputTokens != null) tokRows.push(Row({ title: 'output', meta: fmtTok(usage.outputTokens) + ' tok' }));
|
|
103
|
-
if (usage.costUsd != null) tokRows.push(Row({ title: 'cost', meta: '$' + usage.costUsd.toFixed(4) }));
|
|
104
|
-
if (usage.turns != null) tokRows.push(Row({ title: 'turns', meta: String(usage.turns) }));
|
|
105
|
-
// One duration vocabulary kit-wide: shared fmtDuration (s -> m -> h).
|
|
106
|
-
if (usage.durationMs != null) tokRows.push(Row({ title: 'duration', meta: fmtDuration(usage.durationMs) }));
|
|
107
|
-
panels.push(h('div', { class: 'ds-context-group' },
|
|
108
|
-
h('div', { class: 'ds-context-group-label' }, 'last turn'),
|
|
109
|
-
...tokRows));
|
|
110
|
-
}
|
|
111
|
-
// Recent files: files touched by tool calls this session, most-recent
|
|
112
|
-
// first, capped to 5 rows so the panel stays a glance not a log. Lighter
|
|
113
|
-
// fact group, not a full Panel - same reasoning as above.
|
|
114
|
-
if (Array.isArray(recentFiles) && recentFiles.length) {
|
|
115
|
-
const fileRows = recentFiles.slice(0, 5).map((f) => Row({
|
|
116
|
-
title: f.path.split(/[/\\]/).filter(Boolean).pop() || f.path,
|
|
117
|
-
sub: f.path,
|
|
118
|
-
meta: f.time || undefined,
|
|
119
|
-
onClick: onOpenFile ? () => onOpenFile(f.path) : undefined,
|
|
120
|
-
}));
|
|
121
|
-
panels.push(h('div', { class: 'ds-context-group' },
|
|
122
|
-
h('div', { class: 'ds-context-group-label' }, 'recent files'),
|
|
123
|
-
...fileRows));
|
|
124
|
-
}
|
|
125
|
-
// The cwd action lives on the working-dir row above; no floating footer button.
|
|
126
|
-
return h('div', { class: 'ds-context' }, ...panels);
|
|
127
|
-
}
|
|
1
|
+
// context-pane barrel — thin barrel over ./context-pane/*.js. ContextPane's
|
|
2
|
+
// public export name/shape is unchanged for existing consumers; ContextMeter,
|
|
3
|
+
// ContextTreemap (+ its squarify layout helper) and ContextXRayPanel are new
|
|
4
|
+
// additions living in their own single-responsibility submodules alongside
|
|
5
|
+
// it, split out purely to respect the 200-line module cap.
|
|
6
|
+
|
|
7
|
+
import { ContextPane } from './context-pane/pane.js';
|
|
8
|
+
import { ContextMeter } from './context-pane/meter.js';
|
|
9
|
+
import { ContextTreemap, squarify } from './context-pane/treemap.js';
|
|
10
|
+
import { ContextXRayPanel } from './context-pane/xray.js';
|
|
11
|
+
|
|
12
|
+
export { ContextPane, ContextMeter, ContextTreemap, squarify, ContextXRayPanel };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Progress — plain value/max progress bar. Sibling module of data-density.js
|
|
2
|
+
// (kept separate purely to respect the 200-line module cap, not a
|
|
3
|
+
// barrel-over-submodules split of the whole group): the percentage-width
|
|
4
|
+
// bar-fill approach is the same one BarRow uses in data-density.js, just
|
|
5
|
+
// without BarRow's label/meta column chrome — track + fill only.
|
|
6
|
+
|
|
7
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
8
|
+
const h = webjsx.createElement;
|
|
9
|
+
|
|
10
|
+
export function Progress({ value = 0, max = 100, label } = {}) {
|
|
11
|
+
const pct = max > 0 ? Math.max(0, Math.min(100, (value / max) * 100)) : 0;
|
|
12
|
+
return h('div', {
|
|
13
|
+
class: 'ds-progress', role: 'progressbar',
|
|
14
|
+
'aria-valuenow': String(value), 'aria-valuemin': '0', 'aria-valuemax': String(max),
|
|
15
|
+
'aria-label': label != null ? String(label) : 'progress',
|
|
16
|
+
},
|
|
17
|
+
label ? h('span', { class: 'ds-progress-label' }, label) : null,
|
|
18
|
+
h('div', { class: 'ds-progress-track' },
|
|
19
|
+
h('div', { class: 'ds-progress-fill', style: `width:${pct}%` })));
|
|
20
|
+
}
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
import * as webjsx from '../../vendor/webjsx/index.js';
|
|
10
10
|
import { Pill } from './shell.js';
|
|
11
|
+
import { Progress } from './data-density/progress.js';
|
|
11
12
|
const h = webjsx.createElement;
|
|
12
13
|
|
|
14
|
+
export { Progress };
|
|
15
|
+
|
|
13
16
|
// ---------------------------------------------------------------------------
|
|
14
17
|
// PhaseWalk — compact horizontal phase-progress indicator.
|
|
15
18
|
// phases : ordered phase names (default the 5-stage gm chain).
|
|
@@ -87,3 +87,15 @@ export function Divider({ label, vertical = false, key } = {}) {
|
|
|
87
87
|
return h('div', { key, class: 'ds-ep-divider ds-ep-divider-labeled', role: 'separator' },
|
|
88
88
|
h('span', { class: 'ds-ep-divider-label' }, label));
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// AspectRatio — thin wrapper over the `.ds-aspect` CSS utility (app-shell's
|
|
93
|
+
// base.css), matching Divider's own trivial-CSS-only-primitive-still-gets-a-
|
|
94
|
+
// factory convention. `ratio` accepts a CSS ratio string ('1/1', '16/9') or
|
|
95
|
+
// a number (interpreted as width/height); falls back to the utility's own
|
|
96
|
+
// 16/9 default when omitted.
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
export function AspectRatio({ ratio, children, key } = {}) {
|
|
99
|
+
const cssRatio = typeof ratio === 'number' ? `${ratio} / 1` : ratio;
|
|
100
|
+
return h('div', { key, class: 'ds-aspect', style: cssRatio ? `--aspect:${cssRatio}` : null }, children);
|
|
101
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { Toolbar, ToolbarRow, Tabs, IconButtonGroup } from './editor-primitives/chrome.js';
|
|
12
12
|
import { TreeView, TreeItem } from './editor-primitives/tree.js';
|
|
13
13
|
import { PropertyGrid, PropertyField, PropertyGridRow, InlineEditableField } from './editor-primitives/property-grid.js';
|
|
14
|
-
import { Dock, BP_SM, BP_MD, BP_LG, BP_XL, useMediaQuery, Grid, GridItem, Divider } from './editor-primitives/layout.js';
|
|
14
|
+
import { Dock, BP_SM, BP_MD, BP_LG, BP_XL, useMediaQuery, Grid, GridItem, Divider, AspectRatio } from './editor-primitives/layout.js';
|
|
15
15
|
import { ResizeHandle, SplitPanel } from './editor-primitives/split-panel.js';
|
|
16
16
|
import { Collapse, CollapseGroup } from './editor-primitives/collapse.js';
|
|
17
17
|
import { FocusTrap } from './editor-primitives/focus-trap.js';
|
|
@@ -27,7 +27,7 @@ export {
|
|
|
27
27
|
Toolbar, ToolbarRow, Tabs, IconButtonGroup,
|
|
28
28
|
TreeView, TreeItem,
|
|
29
29
|
PropertyGrid, PropertyField, PropertyGridRow, InlineEditableField,
|
|
30
|
-
Dock, BP_SM, BP_MD, BP_LG, BP_XL, useMediaQuery, Grid, GridItem, Divider,
|
|
30
|
+
Dock, BP_SM, BP_MD, BP_LG, BP_XL, useMediaQuery, Grid, GridItem, Divider, AspectRatio,
|
|
31
31
|
ResizeHandle, SplitPanel,
|
|
32
32
|
Collapse, CollapseGroup,
|
|
33
33
|
FocusTrap,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// HoverCard — Popover composed with a delayed hover trigger. Wraps `trigger`
|
|
2
|
+
// in a span with pointerenter/pointerleave listeners that manage open state
|
|
3
|
+
// via a pair of setTimeout delays (open delay on enter, close delay on
|
|
4
|
+
// leave), clearing any pending timer before scheduling a new one so a quick
|
|
5
|
+
// re-enter cancels a pending close. Popover itself is controlled — this
|
|
6
|
+
// component owns the open/close timers and calls back into the caller so the
|
|
7
|
+
// caller can hold `open` in state (same controlled-component pattern as
|
|
8
|
+
// every other overlay in this group).
|
|
9
|
+
|
|
10
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
11
|
+
import { Popover } from './popover.js';
|
|
12
|
+
import { kids } from './floating.js';
|
|
13
|
+
const h = webjsx.createElement;
|
|
14
|
+
|
|
15
|
+
const _timers = new WeakMap();
|
|
16
|
+
|
|
17
|
+
function _clear(el) {
|
|
18
|
+
const t = _timers.get(el);
|
|
19
|
+
if (t) { clearTimeout(t.open); clearTimeout(t.close); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// HoverCard({ trigger, content, open, onOpenChange, openDelay=700, closeDelay=300, placement, ariaLabel })
|
|
23
|
+
// trigger : a single webjsx VElement (e.g. a link or button) — the hover target.
|
|
24
|
+
// content : Popover children (rendered only while open).
|
|
25
|
+
// open : caller-owned boolean — HoverCard schedules onOpenChange(true/false)
|
|
26
|
+
// via the hover timers rather than holding its own state.
|
|
27
|
+
// onOpenChange : callback(nextOpen) — required to actually see the card open.
|
|
28
|
+
// The wrapper span itself doubles as the Popover anchor element. Popover is
|
|
29
|
+
// imperative (it owns a WeakMap keyed on anchorEl and creates/tears down its
|
|
30
|
+
// own body-level node outside the webjsx diff), so it is invoked from the
|
|
31
|
+
// anchor's ref callback via queueMicrotask rather than inline during this
|
|
32
|
+
// declarative render pass — mirrors CommandPalette/EmojiPicker's own
|
|
33
|
+
// ref-then-queueMicrotask imperative-child pattern in this same group.
|
|
34
|
+
export function HoverCard({ trigger, content, open, onOpenChange, openDelay = 700, closeDelay = 300, placement = 'top', ariaLabel } = {}) {
|
|
35
|
+
const child = kids(trigger)[0];
|
|
36
|
+
if (!child) return null;
|
|
37
|
+
const schedule = (el, kind, ms) => {
|
|
38
|
+
_clear(el);
|
|
39
|
+
const timers = _timers.get(el) || {};
|
|
40
|
+
if (kind === 'open') timers.open = setTimeout(() => onOpenChange && onOpenChange(true), ms);
|
|
41
|
+
else timers.close = setTimeout(() => onOpenChange && onOpenChange(false), ms);
|
|
42
|
+
_timers.set(el, timers);
|
|
43
|
+
};
|
|
44
|
+
const runPopover = (el) => Popover({
|
|
45
|
+
open: Boolean(open), anchorEl: el, onClose: () => onOpenChange && onOpenChange(false),
|
|
46
|
+
placement, ariaLabel, children: content,
|
|
47
|
+
});
|
|
48
|
+
const anchorRef = (el) => {
|
|
49
|
+
if (!el) return;
|
|
50
|
+
queueMicrotask(() => runPopover(el));
|
|
51
|
+
if (el._dsHoverCard) return;
|
|
52
|
+
el._dsHoverCard = true;
|
|
53
|
+
el.addEventListener('pointerenter', () => schedule(el, 'open', openDelay));
|
|
54
|
+
el.addEventListener('pointerleave', () => schedule(el, 'close', closeDelay));
|
|
55
|
+
el.addEventListener('focusin', () => schedule(el, 'open', openDelay));
|
|
56
|
+
el.addEventListener('focusout', () => schedule(el, 'close', closeDelay));
|
|
57
|
+
};
|
|
58
|
+
return h('span', { class: 'ds-hovercard', ref: anchorRef },
|
|
59
|
+
webjsx.createElement(child.type, { ...(child.props || {}) }, ...(child.children || [])));
|
|
60
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Menubar — a horizontal row of Dropdown-style menus (built on the same
|
|
2
|
+
// useRovingMenu state machine that ./menus.js's Dropdown uses) sharing
|
|
3
|
+
// cross-menu-hover-switch state. Dropdown itself keeps its open/close state
|
|
4
|
+
// private (no onOpenChange prop), so a true hover-driven switch — closing
|
|
5
|
+
// whichever menu is open and opening the one now under the pointer, without
|
|
6
|
+
// a second click — needs the shared state machine directly rather than a
|
|
7
|
+
// black-box Dropdown instance per item; each trigger still renders the exact
|
|
8
|
+
// role=menu / .ds-dropdown-item DOM Dropdown itself builds, so it is visually
|
|
9
|
+
// and semantically identical to a row of Dropdowns.
|
|
10
|
+
//
|
|
11
|
+
// Controlled component: the caller owns `openIndex`/`onOpenIndexChange`
|
|
12
|
+
// (which menu, if any, is open), matching the rest of this overlay group.
|
|
13
|
+
|
|
14
|
+
import * as webjsx from '../../../vendor/webjsx/index.js';
|
|
15
|
+
import { useRovingMenu } from './roving-menu.js';
|
|
16
|
+
const h = webjsx.createElement;
|
|
17
|
+
|
|
18
|
+
function buildMenuEl(items, onSelect, close, ariaLabel) {
|
|
19
|
+
const el = document.createElement('div');
|
|
20
|
+
el.className = 'ds-popover ds-dropdown-menu';
|
|
21
|
+
el.setAttribute('role', 'menu');
|
|
22
|
+
if (ariaLabel) el.setAttribute('aria-label', ariaLabel);
|
|
23
|
+
const select = (it) => { if (it.disabled || it.separator) return; if (onSelect) onSelect(it.id, it); close(); };
|
|
24
|
+
const tree = h('div', { class: 'ds-dropdown-list' },
|
|
25
|
+
...items.map((it, i) => it.separator
|
|
26
|
+
? h('div', { key: 'sep' + i, class: 'ds-dropdown-separator', role: 'separator' })
|
|
27
|
+
: h('button', {
|
|
28
|
+
key: it.id || i, type: 'button', role: 'menuitem',
|
|
29
|
+
class: 'ds-dropdown-item' + (it.danger ? ' is-danger' : ''),
|
|
30
|
+
'aria-disabled': it.disabled ? 'true' : 'false',
|
|
31
|
+
tabindex: '-1', onclick: () => select(it),
|
|
32
|
+
}, h('span', { class: 'ds-dropdown-label' }, it.label))));
|
|
33
|
+
webjsx.applyDiff(el, tree);
|
|
34
|
+
return el;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Menubar({ menus, openIndex, onOpenIndexChange })
|
|
38
|
+
// menus: [{ id?, label, items, onSelect }].
|
|
39
|
+
// openIndex: caller-owned index of the currently open menu, or null.
|
|
40
|
+
// onOpenIndexChange(next): called with the new open index (or null on close).
|
|
41
|
+
export function Menubar({ menus = [], openIndex = null, onOpenIndexChange, ariaLabel = 'Menu bar' } = {}) {
|
|
42
|
+
return h('div', { class: 'ds-menubar', role: 'menubar', 'aria-label': ariaLabel },
|
|
43
|
+
...menus.map((menu, i) => {
|
|
44
|
+
const isOpen = openIndex === i;
|
|
45
|
+
const roving = useRovingMenu({
|
|
46
|
+
itemSelector: '[role="menuitem"]:not([aria-disabled="true"])',
|
|
47
|
+
items: menu.items || [], typeahead: true, placement: 'bottom-start',
|
|
48
|
+
onOpenChange: (open) => onOpenIndexChange && onOpenIndexChange(open ? i : null),
|
|
49
|
+
});
|
|
50
|
+
const open = () => roving.openMenu(() => buildMenuEl(menu.items || [], menu.onSelect, roving.close, menu.label), true);
|
|
51
|
+
const onTrigClick = () => { if (roving.isOpen()) roving.close(); else open(); };
|
|
52
|
+
const onTrigKey = (e) => { if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (!roving.isOpen()) open(); else roving.focusItem(0); } };
|
|
53
|
+
// Cross-menu hover switch: only steal focus when a DIFFERENT menu
|
|
54
|
+
// is already open — plain hover with nothing open still requires
|
|
55
|
+
// a click, matching standard menubar behavior.
|
|
56
|
+
const onEnter = () => { if (openIndex != null && openIndex !== i && !roving.isOpen()) open(); };
|
|
57
|
+
const refFn = roving.refFn('_dsMenubarTrig');
|
|
58
|
+
const wireRef = (el) => { refFn(el); if (el) { el.addEventListener('click', onTrigClick); el.addEventListener('keydown', onTrigKey); } };
|
|
59
|
+
return h('div', { key: menu.id || i, class: 'ds-menubar-item', onpointerenter: onEnter },
|
|
60
|
+
h('button', {
|
|
61
|
+
type: 'button', class: 'ds-menubar-trigger' + (isOpen ? ' is-open' : ''),
|
|
62
|
+
role: 'menuitem', ref: wireRef,
|
|
63
|
+
}, menu.label));
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
@@ -21,6 +21,8 @@ import { EmojiPicker } from './overlay-primitives/emoji-picker.js';
|
|
|
21
21
|
import { SettingsPopover } from './overlay-primitives/settings-popover.js';
|
|
22
22
|
import { AuthModal } from './overlay-primitives/auth-modal.js';
|
|
23
23
|
import { BootOverlay, VideoLightbox } from './overlay-primitives/full-screen.js';
|
|
24
|
+
import { HoverCard } from './overlay-primitives/hover-card.js';
|
|
25
|
+
import { Menubar } from './overlay-primitives/menubar.js';
|
|
24
26
|
|
|
25
27
|
export {
|
|
26
28
|
useFloating, useLongPress, withBusy, trapTab,
|
|
@@ -34,4 +36,6 @@ export {
|
|
|
34
36
|
SettingsPopover,
|
|
35
37
|
AuthModal,
|
|
36
38
|
BootOverlay, VideoLightbox,
|
|
39
|
+
HoverCard,
|
|
40
|
+
Menubar,
|
|
37
41
|
};
|