anentrypoint-design 0.0.414 → 0.0.416

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.
Files changed (102) hide show
  1. package/dist/247420.js +62 -12
  2. package/dist/app-shell.css +40 -594
  3. package/dist/colors_and_type.css +607 -47
  4. package/dist/index.html +227 -277
  5. package/dist/preview/buttons.html +39 -28
  6. package/dist/preview/colors-core.html +18 -8
  7. package/dist/preview/colors-lore.html +26 -17
  8. package/dist/preview/colors-semantic.html +24 -18
  9. package/dist/preview/dateline.html +6 -5
  10. package/dist/preview/dropzone.html +4 -3
  11. package/dist/preview/file-grid.html +10 -9
  12. package/dist/preview/file-row.html +14 -13
  13. package/dist/preview/file-toolbar.html +4 -3
  14. package/dist/preview/file-viewer.html +4 -3
  15. package/dist/preview/header.html +26 -24
  16. package/dist/preview/icons-unicode.html +8 -5
  17. package/dist/preview/index-row.html +17 -9
  18. package/dist/preview/inputs.html +4 -3
  19. package/dist/preview/manifesto.html +34 -17
  20. package/dist/preview/motion-default.js +77 -57
  21. package/dist/preview/rules.html +4 -3
  22. package/dist/preview/spacing.html +13 -12
  23. package/dist/preview/stamps-lore.html +5 -4
  24. package/dist/preview/stamps.html +4 -3
  25. package/dist/preview/theme-ink.html +7 -6
  26. package/dist/preview/type-display.html +14 -5
  27. package/dist/preview/type-mono.html +6 -5
  28. package/dist/preview/type-prose.html +12 -10
  29. package/dist/preview/type-scale.html +4 -3
  30. package/dist/preview/wordmarks.html +10 -7
  31. package/dist/site/content/globals/site.yaml +5 -3
  32. package/dist/site/content/pages/freddie.yaml +2 -2
  33. package/dist/site/content/pages/home.yaml +55 -55
  34. package/dist/site/theme.mjs +96 -348
  35. package/dist/slides/index.html +13 -13
  36. package/dist/src/bootstrap.js +10 -1
  37. package/dist/src/components/chat.js +43 -195
  38. package/dist/src/components/community.js +18 -166
  39. package/dist/src/components/content.js +30 -210
  40. package/dist/src/components/files-modals.js +14 -106
  41. package/dist/src/components/files.js +15 -116
  42. package/dist/src/components/freddie/helpers.js +53 -8
  43. package/dist/src/components/freddie.js +35 -25
  44. package/dist/src/components/shell.js +16 -114
  45. package/dist/src/components/theme-toggle.js +69 -25
  46. package/dist/src/components.js +92 -13
  47. package/dist/src/deck-stage.js +6 -4
  48. package/dist/src/highlight.js +51 -7
  49. package/dist/src/index.js +58 -12
  50. package/dist/src/kits/os/about-app.js +2 -3
  51. package/dist/src/kits/os/app-panes.css +46 -70
  52. package/dist/src/kits/os/browser-app.js +28 -1
  53. package/dist/src/kits/os/files-app.js +7 -0
  54. package/dist/src/kits/os/freddie/pages-chat.js +90 -75
  55. package/dist/src/kits/os/freddie/pages-core.js +9 -9
  56. package/dist/src/kits/os/freddie/pages-os.js +8 -8
  57. package/dist/src/kits/os/freddie/pages-tools.js +14 -14
  58. package/dist/src/kits/os/freddie/routes.js +21 -19
  59. package/dist/src/kits/os/freddie-dashboard.css +42 -21
  60. package/dist/src/kits/os/freddie-dashboard.js +6 -6
  61. package/dist/src/kits/os/icons.js +38 -16
  62. package/dist/src/kits/os/index.js +11 -0
  63. package/dist/src/kits/os/launcher.css +26 -10
  64. package/dist/src/kits/os/launcher.js +3 -0
  65. package/dist/src/kits/os/shell.js +72 -108
  66. package/dist/src/kits/os/terminal-app.js +1 -1
  67. package/dist/src/kits/os/theme.css +1330 -150
  68. package/dist/src/kits/os/wm.css +68 -15
  69. package/dist/src/kits/os/wm.js +93 -17
  70. package/dist/src/markdown.js +121 -12
  71. package/dist/src/motion.js +18 -2
  72. package/dist/src/page-html.js +62 -134
  73. package/dist/src/theme.js +106 -13
  74. package/dist/src/web-components/ds-chat.js +80 -9
  75. package/dist/ui_kits/aicat/app.js +54 -6
  76. package/dist/ui_kits/aicat/index.html +2 -0
  77. package/dist/ui_kits/blog/index.html +33 -17
  78. package/dist/ui_kits/chat/app.js +90 -22
  79. package/dist/ui_kits/chat/index.html +2 -0
  80. package/dist/ui_kits/community/app.js +86 -24
  81. package/dist/ui_kits/community/index.html +1 -0
  82. package/dist/ui_kits/dashboard/app.js +123 -34
  83. package/dist/ui_kits/dashboard/index.html +1 -0
  84. package/dist/ui_kits/docs/index.html +43 -33
  85. package/dist/ui_kits/error_404/app.js +78 -23
  86. package/dist/ui_kits/file_browser/README.md +1 -1
  87. package/dist/ui_kits/file_browser/app.js +45 -10
  88. package/dist/ui_kits/file_browser/index.html +1 -0
  89. package/dist/ui_kits/gallery/app.js +130 -51
  90. package/dist/ui_kits/homepage/app.js +156 -138
  91. package/dist/ui_kits/project_page/app.js +142 -125
  92. package/dist/ui_kits/project_page/index.html +2 -1
  93. package/dist/ui_kits/search/app.js +52 -15
  94. package/dist/ui_kits/settings/app.js +166 -39
  95. package/dist/ui_kits/signin/app.js +175 -49
  96. package/dist/ui_kits/signin/index.html +1 -0
  97. package/dist/ui_kits/slide_deck/app.js +83 -49
  98. package/dist/ui_kits/system_primer/app.js +57 -40
  99. package/dist/ui_kits/terminal/app.js +160 -43
  100. package/package.json +1 -1
  101. package/src/page-html/client-script.js +40 -11
  102. package/src/page-html/page-styles.js +22 -1
package/dist/src/index.js CHANGED
@@ -5,15 +5,28 @@
5
5
  import * as webjsx from '../vendor/webjsx/index.js';
6
6
  import { loadCss, scope } from './styles.js';
7
7
  import { registerDeckStage, getDeckStage } from './deck-stage.js';
8
+ import { Router, createRouter } from './router.js';
8
9
  import * as components from './components.js';
9
10
  import * as motion from './motion.js';
10
11
  import * as debug from './debug.js';
11
- import { renderMarkdown, ensureReady as ensureMarkdownReady } from './markdown.js';
12
- import { ensurePrism, highlightAllUnder } from './highlight.js';
12
+ import { renderMarkdown, ensureReady as ensureMarkdownReady, sanitizeHtml, isDegraded as isMarkdownDegraded, configureMarkdownCdn, getMarkdownCdnConfig } from './markdown.js';
13
+ import { escapeHtml, escapeJson } from './html-escape.js';
14
+ import { uid, shortUid } from './uid.js';
15
+ import { ensurePrism, highlightAllUnder, configurePrismCdn, getPrismCdnConfig } from './highlight.js';
16
+ import { ensureMermaid, renderMermaid, renderMermaidBlocksUnder, configureMermaidCdn, getMermaidCdnConfig } from './mermaid.js';
17
+ import { ensureKatex, renderMathBlocksUnder, configureKatexCdn, getKatexCdnConfig } from './math.js';
13
18
  import { renderPageHtml } from './page-html.js';
19
+ import { HeroFromPageData } from './components/content.js';
20
+ import { ThemeToggle } from './components/theme-toggle.js';
14
21
  import { mountKit } from './bootstrap.js';
15
22
  import * as theme from './theme.js';
23
+ import { t, registerLocale, getLocale, setLocale, availableLocales } from './i18n.js';
16
24
  import { registerChatElement, DsChat } from './web-components/ds-chat.js';
25
+ import { registerFreddieChatElement, FreddieChat } from './web-components/freddie-chat.js';
26
+ import { formatTime, formatDateTime, formatNumber, formatRelativeTime } from './locale.js';
27
+ import { queueMessage, listQueued, flushQueue, watchReconnect, isOnline } from './idb-outbox.js';
28
+ import { createVirtualizer, measureRef } from './virtual-scroll.js';
29
+ import { applyMotion, getMotion, isMotionReduced, onMotionChange, initMotion } from './motion-toggle.js';
17
30
 
18
31
  let _installed = false;
19
32
  export async function installStyles(target) {
@@ -32,9 +45,18 @@ export async function installStyles(target) {
32
45
  }
33
46
  }
34
47
 
48
+ // Tracks nodes already mounted via mount() so a second mount() call onto the
49
+ // same DOM node fails loud instead of silently layering a second render loop
50
+ // (double applyDiff/animateTree on one root corrupts webjsx's diff state).
51
+ const _mountedRoots = new WeakSet();
52
+
35
53
  export function mount(rootEl, viewFn, { autoScope = true } = {}) {
36
- if (!rootEl) throw new Error('mount: rootEl required');
54
+ if (!rootEl) throw new Error('mount: rootEl required (received ' + (rootEl === null ? 'null' : typeof rootEl) + ')');
37
55
  if (typeof viewFn !== 'function') throw new Error('mount: viewFn required');
56
+ if (_mountedRoots.has(rootEl)) {
57
+ throw new Error('mount: this element is already mounted — call the returned render() to re-render, do not mount() the same root twice');
58
+ }
59
+ _mountedRoots.add(rootEl);
38
60
  if (autoScope && rootEl.classList && !rootEl.classList.contains(scope.slice(1))) {
39
61
  const cls = scope.slice(1);
40
62
  const inheritedFromAncestor = rootEl.closest && rootEl.closest('.' + cls);
@@ -51,36 +73,60 @@ export function mount(rootEl, viewFn, { autoScope = true } = {}) {
51
73
  return render;
52
74
  }
53
75
 
54
- // Side-effect: register <ds-chat> as soon as the SDK loads in a browser.
76
+ // Side-effect: register <ds-chat> + <freddie-chat> as soon as the SDK loads in a browser.
55
77
  if (typeof window !== 'undefined' && typeof customElements !== 'undefined') {
56
78
  registerChatElement();
79
+ registerFreddieChatElement();
57
80
  }
58
81
 
59
82
  export {
60
83
  webjsx, loadCss, scope,
61
84
  registerDeckStage, getDeckStage,
85
+ Router, createRouter,
62
86
  components, motion, debug, mountKit,
63
- renderMarkdown, ensureMarkdownReady,
64
- ensurePrism, highlightAllUnder,
87
+ renderMarkdown, ensureMarkdownReady, sanitizeHtml, isMarkdownDegraded,
88
+ configureMarkdownCdn, getMarkdownCdnConfig,
89
+ ensurePrism, highlightAllUnder, configurePrismCdn, getPrismCdnConfig,
90
+ ensureMermaid, renderMermaid, renderMermaidBlocksUnder, configureMermaidCdn, getMermaidCdnConfig,
91
+ ensureKatex, renderMathBlocksUnder, configureKatexCdn, getKatexCdnConfig,
65
92
  registerChatElement, DsChat,
66
- renderPageHtml,
67
- theme
93
+ registerFreddieChatElement, FreddieChat,
94
+ renderPageHtml, HeroFromPageData,
95
+ escapeHtml, escapeJson,
96
+ uid, shortUid,
97
+ theme, ThemeToggle,
98
+ t, registerLocale, getLocale, setLocale, availableLocales,
99
+ formatTime, formatDateTime, formatNumber, formatRelativeTime,
100
+ queueMessage, listQueued, flushQueue, watchReconnect, isOnline,
101
+ createVirtualizer, measureRef,
102
+ applyMotion, getMotion, isMotionReduced, onMotionChange, initMotion
68
103
  };
69
- export { applyTheme, getTheme, resolvedTheme, onThemeChange, initTheme } from './theme.js';
104
+ export { applyTheme, getTheme, resolvedTheme, onThemeChange, initTheme,
105
+ applyAccent, getAccent, applyDensity, getDensity } from './theme.js';
106
+ export { extractAtQuery, buildEntriesFromFiles, filterFileEntries, buildAtInsertText,
107
+ buildAtMentionText, buildFileAtMentionsText } from './file-mention.js';
70
108
  export const h = webjsx.createElement;
71
109
  export const applyDiff = webjsx.applyDiff;
72
110
 
111
+ // spoint kit paint surfaces (loading screen, HUD, editor chrome).
112
+ export { renderLoadingScreen } from './kits/spoint/loading-screen.js';
113
+ export { renderGameHud, Crosshair, AmmoCounter, HealthBar, BoostIndicator } from './kits/spoint/game-hud.js';
114
+ export { renderHostJoinLobby } from './kits/spoint/host-join-lobby.js';
115
+
73
116
  // Re-export freddie helpers so consumers can `import { FREDDIE_PAGES } from
74
117
  // 'anentrypoint-design'` directly.
75
118
  export {
76
119
  FREDDIE_PAGES,
77
120
  home, chat, voice, sessions, projects, agents, analytics,
78
121
  models, cron, skills, config, env, tools, batch, gateway, chains,
79
- skillLabel, getRecentPaths, saveRecentPath, renderChatMessages
122
+ skillLabel, getRecentPaths, saveRecentPath, renderChatMessages,
123
+ fmtBytes, fmtFileSize, fmtTime, fmtAgo, fmtDuration
80
124
  } from './components.js';
81
125
 
82
126
  export default {
83
127
  webjsx, loadCss, scope, installStyles, mount, h, applyDiff,
84
- registerDeckStage, getDeckStage, components, motion, debug, mountKit,
85
- renderMarkdown, ensurePrism, registerChatElement, renderPageHtml
128
+ registerDeckStage, getDeckStage, Router, createRouter,
129
+ components, motion, debug, mountKit,
130
+ renderMarkdown, ensurePrism, registerChatElement, renderPageHtml, HeroFromPageData,
131
+ escapeHtml, escapeJson
86
132
  };
@@ -13,10 +13,9 @@ export function renderAboutApp(opts = {}) {
13
13
  'libsql via sql.js · freddie host',
14
14
  'responsive: phone / tablet / desktop',
15
15
  ],
16
- footer = 'open <code>validator</code> to run isolation harness · click apps menu for more.',
16
+ footer = 'click apps menu for more.',
17
17
  links = [
18
18
  { href: 'https://github.com/AnEntrypoint/thebird', text: 'source' },
19
- { href: './validate.html', text: 'validate' },
20
19
  ],
21
20
  } = opts;
22
21
 
@@ -35,7 +34,7 @@ export function renderAboutApp(opts = {}) {
35
34
  ul.appendChild(li);
36
35
  }
37
36
  const foot = document.createElement('p');
38
- foot.innerHTML = footer;
37
+ foot.textContent = footer;
39
38
  const meta = document.createElement('p');
40
39
  meta.className = 'meta';
41
40
  links.forEach((l, i) => {
@@ -1,7 +1,7 @@
1
1
  /* App-pane chrome for kit components rendered inside .wm-win bodies.
2
2
  Bible tokens only — no hardcoded color, font, or radius. Focus = inset rail; no shadows, no gradients. */
3
3
 
4
- /* ── terminal-app ───────────────────────────────────────────────────── */
4
+ /* --- terminal-app --- */
5
5
  .terminal-app {
6
6
  display: flex;
7
7
  flex-direction: column;
@@ -14,7 +14,8 @@
14
14
  display: flex;
15
15
  align-items: center;
16
16
  justify-content: space-between;
17
- padding: 4px 10px;
17
+ gap: var(--space-2);
18
+ padding: var(--space-1) var(--space-2);
18
19
  background: var(--os-bg-2);
19
20
  font-family: var(--os-mono);
20
21
  font-size: 11px;
@@ -22,16 +23,32 @@
22
23
  text-transform: lowercase;
23
24
  flex: 0 0 auto;
24
25
  }
25
- .terminal-app-title { font-family: var(--os-mono); }
26
- .terminal-app-status { font-family: var(--os-mono); color: var(--os-fg-3); }
26
+ .terminal-app-title {
27
+ font-family: var(--os-mono);
28
+ min-width: 0;
29
+ overflow: hidden;
30
+ text-overflow: ellipsis;
31
+ white-space: nowrap;
32
+ }
33
+ .terminal-app-status {
34
+ font-family: var(--os-mono);
35
+ color: var(--os-fg-3);
36
+ flex: 0 0 auto;
37
+ overflow: hidden;
38
+ text-overflow: ellipsis;
39
+ white-space: nowrap;
40
+ }
27
41
  .terminal-app-slot {
28
42
  flex: 1 1 auto;
43
+ min-width: 0;
29
44
  min-height: 0;
30
45
  overflow: hidden;
31
46
  background: var(--os-bg-0);
47
+ position: relative;
32
48
  }
49
+ .terminal-app-slot > * { width: 100%; height: 100%; }
33
50
 
34
- /* ── browser-app ────────────────────────────────────────────────────── */
51
+ /* --- browser-app --- */
35
52
  .browser-app {
36
53
  display: flex;
37
54
  flex-direction: column;
@@ -43,15 +60,15 @@
43
60
  .browser-app-bar {
44
61
  display: flex;
45
62
  align-items: center;
46
- gap: 6px;
47
- padding: 6px 8px;
63
+ gap: var(--space-1);
64
+ padding: var(--space-1) var(--space-2);
48
65
  background: var(--os-bg-2);
49
66
  flex: 0 0 auto;
50
67
  }
51
68
  .browser-app-btn {
52
69
  height: 28px;
53
70
  min-width: 28px;
54
- padding: 0 8px;
71
+ padding: 0 var(--space-2);
55
72
  background: var(--os-bg-3);
56
73
  color: var(--os-fg);
57
74
  border: none;
@@ -61,10 +78,15 @@
61
78
  outline: none;
62
79
  }
63
80
  .browser-app-btn:hover { background: var(--os-accent-soft); }
81
+ .browser-app-btn:focus-visible { box-shadow: inset 0 0 0 1px var(--os-accent); }
82
+ /* Inert back/forward when there's no history in that direction. */
83
+ .browser-app-btn:disabled { opacity: 0.4; cursor: default; pointer-events: none; }
84
+ .browser-app-btn:disabled:hover { background: var(--os-bg-3); }
64
85
  .browser-app-url {
65
86
  flex: 1 1 auto;
87
+ min-width: 0;
66
88
  height: 28px;
67
- padding: 0 10px;
89
+ padding: 0 var(--space-2);
68
90
  background: var(--os-bg-0);
69
91
  color: var(--os-fg);
70
92
  border: none;
@@ -75,78 +97,32 @@
75
97
  .browser-app-url:focus { box-shadow: inset 4px 0 0 var(--os-accent); }
76
98
  .browser-app-slot {
77
99
  flex: 1 1 auto;
100
+ min-width: 0;
78
101
  min-height: 0;
79
102
  overflow: hidden;
80
103
  background: var(--os-bg-0);
81
- display: flex;
104
+ position: relative;
82
105
  }
83
- .browser-app-slot > * { flex: 1 1 auto; min-width: 0; min-height: 0; }
106
+ .browser-app-slot > * { width: 100%; height: 100%; }
84
107
  .browser-app-status {
85
108
  flex: 0 0 auto;
86
- padding: 2px 10px;
109
+ padding: var(--space-1) var(--space-2);
87
110
  background: var(--os-bg-2);
88
111
  font: 11px var(--os-mono);
89
112
  color: var(--os-fg-3);
90
113
  text-transform: lowercase;
91
114
  min-height: 18px;
92
115
  }
116
+ /* Loading: a thin indeterminate bar under the toolbar. Error: red status row. */
117
+ .browser-app-loading .browser-app-bar { box-shadow: inset 0 -2px 0 var(--os-accent); }
118
+ .browser-app[data-error] .browser-app-status { color: var(--os-danger, #c0392b); }
93
119
 
94
- /* ── validator-app ──────────────────────────────────────────────────── */
95
- .validator-app {
96
- display: flex;
97
- flex-direction: column;
98
- height: 100%;
99
- background: var(--os-bg-1);
100
- color: var(--os-fg);
101
- font-family: var(--os-font);
102
- }
103
- .validator-app-head {
104
- display: flex;
105
- align-items: center;
106
- gap: 10px;
107
- padding: 6px 10px;
108
- background: var(--os-bg-2);
109
- flex: 0 0 auto;
110
- }
111
- .validator-app-banner {
112
- padding: 2px 10px;
113
- border-radius: 999px;
114
- font: 11px var(--os-mono);
115
- text-transform: lowercase;
116
- background: var(--os-bg-3);
117
- color: var(--os-fg-2);
118
- }
119
- .validator-app-banner[data-state="pass"] { background: var(--os-accent-soft); color: var(--os-fg); }
120
- .validator-app-banner[data-state="fail"] { background: var(--os-red); color: var(--os-bg-0); }
121
- .validator-app-counts {
122
- font: 11px var(--os-mono);
123
- color: var(--os-fg-2);
124
- }
125
- .validator-app-rerun {
126
- margin-left: auto;
127
- height: 24px;
128
- padding: 0 10px;
129
- background: var(--os-bg-3);
130
- color: var(--os-fg);
131
- border: none;
132
- border-radius: var(--os-radius-sm);
133
- font: 11px var(--os-mono);
134
- text-transform: lowercase;
135
- cursor: pointer;
136
- outline: none;
137
- }
138
- .validator-app-rerun:hover { background: var(--os-accent-soft); }
139
- .validator-app-slot {
140
- flex: 1 1 auto;
141
- min-height: 0;
142
- overflow: hidden;
143
- display: flex;
144
- background: var(--os-bg-0);
145
- }
146
- .validator-app-slot > * { flex: 1 1 auto; min-width: 0; min-height: 0; }
147
- .validator-app-frame {
148
- width: 100%;
149
- height: 100%;
150
- border: none;
151
- background: var(--os-bg-0);
120
+ /* Touch tap-target floor for the browser pane toolbar. The back/forward/reload
121
+ * buttons and the URL field are 28px by default (no @media anywhere), below the
122
+ * 44px touch minimum on tablets and phones. Lift them on coarse pointers and
123
+ * grow the bar so the controls fit with breathing room. */
124
+ @media (hover: none), (pointer: coarse) {
125
+ .browser-app-bar { min-height: 52px; padding: 6px var(--space-2); }
126
+ .browser-app-btn { min-height: var(--os-tap, 44px); height: var(--os-tap, 44px); min-width: var(--os-tap, 44px); }
127
+ .browser-app-url { height: var(--os-tap, 44px); }
152
128
  }
@@ -1,7 +1,10 @@
1
1
  // Browser-pane paint surface — URL bar + iframe slot + status row. Consumer owns iframe.
2
2
  // renderBrowserPane({initialUrl, callbacks: {onNavigate, onReload, onBack, onForward}})
3
- // -> {node, slot, setUrl, setStatus, dispose}.
3
+ // -> {node, slot, setUrl, setStatus, setNav, setLoading, setError, dispose}.
4
4
  // slot is the container the consumer should append its iframe to.
5
+ // setNav({canBack,canForward}) disables the back/forward buttons when history
6
+ // has no entry in that direction; setLoading(bool) toggles a loading state on
7
+ // the bar; setError(msg|null) surfaces a load failure in the status row.
5
8
 
6
9
  export function renderBrowserPane(opts = {}) {
7
10
  const { initialUrl = 'about:blank', callbacks = {} } = opts;
@@ -21,8 +24,14 @@ export function renderBrowserPane(opts = {}) {
21
24
  return b;
22
25
  };
23
26
  const backBtn = mkBtn('<', 'back');
27
+ backBtn.setAttribute('aria-label', 'Back');
24
28
  const fwdBtn = mkBtn('>', 'forward');
29
+ fwdBtn.setAttribute('aria-label', 'Forward');
25
30
  const reloadBtn = mkBtn('reload', 'reload');
31
+ reloadBtn.setAttribute('aria-label', 'Reload');
32
+ // Start with no history in either direction until the consumer says otherwise.
33
+ backBtn.disabled = true;
34
+ fwdBtn.disabled = true;
26
35
 
27
36
  const urlInput = document.createElement('input');
28
37
  urlInput.type = 'text';
@@ -44,6 +53,8 @@ export function renderBrowserPane(opts = {}) {
44
53
 
45
54
  const status = document.createElement('div');
46
55
  status.className = 'browser-app-status';
56
+ status.setAttribute('role', 'status');
57
+ status.setAttribute('aria-live', 'polite');
47
58
  status.textContent = '';
48
59
 
49
60
  node.append(bar, slot, status);
@@ -53,6 +64,22 @@ export function renderBrowserPane(opts = {}) {
53
64
  get slot() { return slot; },
54
65
  setUrl(u) { urlInput.value = u; },
55
66
  setStatus(s) { status.textContent = s; },
67
+ // Reflect history availability so disabled buttons read as inert.
68
+ setNav({ canBack = false, canForward = false } = {}) {
69
+ backBtn.disabled = !canBack;
70
+ fwdBtn.disabled = !canForward;
71
+ },
72
+ // Loading: tint the bar + announce; the consumer flips it off on load/error.
73
+ setLoading(on) {
74
+ node.classList.toggle('browser-app-loading', !!on);
75
+ if (on) { node.removeAttribute('data-error'); status.textContent = 'loading...'; }
76
+ },
77
+ // Error: persistent failure surfaced in the live status row.
78
+ setError(msg) {
79
+ node.classList.remove('browser-app-loading');
80
+ if (msg) { node.setAttribute('data-error', '1'); status.textContent = msg; }
81
+ else { node.removeAttribute('data-error'); }
82
+ },
56
83
  dispose() {},
57
84
  };
58
85
  }
@@ -10,8 +10,11 @@ export function renderFilesApp(opts = {}) {
10
10
  node.dataset.component = 'files-app';
11
11
 
12
12
  let preview = null;
13
+ let selected = null;
13
14
  async function refresh() {
14
15
  const items = await list();
16
+ preview = null;
17
+ selected = null;
15
18
  node.innerHTML = '';
16
19
  const head = document.createElement('div');
17
20
  head.className = 'head';
@@ -21,7 +24,11 @@ export function renderFilesApp(opts = {}) {
21
24
  const row = document.createElement('div');
22
25
  row.className = 'row';
23
26
  row.textContent = p;
27
+ row.title = p;
24
28
  row.addEventListener('click', async () => {
29
+ if (selected) selected.classList.remove('selected');
30
+ selected = row;
31
+ row.classList.add('selected');
25
32
  const body = await readFile(p);
26
33
  if (preview) preview.remove();
27
34
  preview = document.createElement('pre');
@@ -1,143 +1,158 @@
1
- // Chat page — its own module because of SSE plumbing weight.
1
+ // Chat page — dashboard surface for the agent. Renders via the kit's
2
+ // Chat + ChatComposer primitives so the dashboard tab and the OS chat panel
3
+ // share the same bubble / tool-call / empty-state chrome. The bespoke
4
+ // cwd/skill/provider/model selectors live in a collapsible config strip
5
+ // above the thread (mirror of the OS panel cc-strip pattern).
6
+ //
7
+ // Wire-format translation lives in ./chat-protocol.js; provider discovery and
8
+ // the three send strategies (server SSE / in-page agent runtime / direct
9
+ // gateway completion) plus the event->state fold live in ./chat-transport.js.
10
+ // This module owns only the page's own state, DOM wiring, and config strip.
11
+
2
12
  import * as webjsx from '../../../../vendor/webjsx/index.js';
3
13
  import * as components from '../../../components.js';
4
- import { getRecentPaths, saveRecentPath, skillLabel, renderChatMessages } from './helpers.js';
14
+ import { getRecentPaths, saveRecentPath, skillLabel } from '../../../components/freddie/helpers.js';
15
+ import { toKitMessage } from './chat-protocol.js';
16
+ import { loadProviders, fetchChatEvents, applyChatEvents } from './chat-transport.js';
5
17
 
6
18
  const h = webjsx.createElement;
7
- const { Panel, Receipt, Chip } = components;
8
-
9
- function parseSseEvents(text) {
10
- const events = [];
11
- let curEvent = null, curData = '';
12
- for (const line of text.split('\n')) {
13
- if (line.startsWith('event: ')) { curEvent = line.slice(7).trim(); }
14
- else if (line.startsWith('data: ')) { curData = line.slice(6).trim(); }
15
- else if (line === '' && curEvent) {
16
- try { events.push({ event: curEvent, data: JSON.parse(curData) }); } catch {}
17
- curEvent = null; curData = '';
18
- }
19
- }
20
- return events;
21
- }
19
+ const { Panel, Receipt, Chip, Icon } = components;
22
20
 
23
21
  export function makeChatPage(ctx) {
24
22
  return async function chat(h0) {
25
23
  const root = ctx.root;
26
24
  const skills = [...h0.pi.skills.values()];
27
- const providers = await fetch('/api/providers').then(r => r.json()).catch(() => []);
25
+ const providers = await loadProviders();
28
26
  const configuredProviders = providers.filter(p => p.configured);
29
27
 
30
28
  const chatState = window.__fd_chatState = window.__fd_chatState || {
31
- cwd: '', skill: '', provider: '', model: '', messages: [], busy: false, sessionId: null,
29
+ cwd: '', skill: '', provider: '', model: '', messages: [], busy: false, sessionId: null, draft: '', abort: null,
32
30
  };
33
31
  if (!chatState.cwd) chatState.cwd = (getRecentPaths()[0] || '');
34
32
 
35
- const getMsgsContainer = () => root.querySelector('#fd-chat-msgs');
33
+ // Find the chat container in the live DOM (set on the rendered <ds-chat>).
34
+ const getChatHost = () => root.querySelector('ds-chat.fd-dashboard-chat');
35
+ const syncMessages = () => {
36
+ const host = getChatHost();
37
+ if (host) host.messages = chatState.messages.map(toKitMessage);
38
+ };
36
39
 
37
40
  const newSession = () => {
38
41
  if (chatState.busy) return;
39
42
  chatState.messages = [];
40
43
  chatState.sessionId = null;
41
- renderChatMessages(getMsgsContainer(), chatState.messages);
44
+ syncMessages();
42
45
  };
43
46
 
44
- const sendChat = async (ev) => {
45
- ev.preventDefault();
47
+ const cancelInFlight = () => {
48
+ if (chatState.abort) { try { chatState.abort.abort(); } catch { /* swallow: the in-flight request may already be settled, abort() is a no-op then */ } chatState.abort = null; }
49
+ chatState.busy = false;
50
+ syncMessages();
51
+ renderPage();
52
+ };
53
+
54
+ const sendChat = async (prompt) => {
46
55
  if (chatState.busy) return;
47
- const promptEl = ev.target.elements.prompt;
48
- const prompt = promptEl.value.trim();
49
- if (!prompt) return;
50
- chatState.messages.push({ role: 'user', content: prompt });
51
- promptEl.value = '';
52
- promptEl.style.height = 'auto';
56
+ const trimmed = String(prompt || '').trim();
57
+ if (!trimmed) return;
58
+ chatState.messages.push({ role: 'user', content: trimmed });
53
59
  chatState.busy = true;
60
+ chatState.progress = 'agent thinking…';
61
+ chatState.abort = new AbortController();
54
62
  saveRecentPath(chatState.cwd);
55
- renderChatMessages(getMsgsContainer(), chatState.messages);
63
+ syncMessages();
64
+ renderPage();
56
65
  try {
57
- const body = { prompt, cwd: chatState.cwd || undefined, skill: chatState.skill || undefined, provider: chatState.provider || undefined, model: chatState.model || undefined, sessionId: chatState.sessionId || undefined };
58
- const resp = await fetch('/api/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
59
- const text = await resp.text();
60
- const events = parseSseEvents(text);
61
- let assistantContent = '';
62
- for (const { event, data } of events) {
63
- if (event === 'start' && data.sessionId) chatState.sessionId = data.sessionId;
64
- if (event === 'done' && data.sessionId) chatState.sessionId = data.sessionId;
65
- if (event === 'message') {
66
- const role = data.role;
67
- if (role === 'assistant') {
68
- const content = Array.isArray(data.content) ? data.content : [{ type: 'text', text: String(data.content || '') }];
69
- for (const block of content) {
70
- if (block.type === 'text') assistantContent += block.text;
71
- if (block.type === 'tool_use') {
72
- if (assistantContent) { chatState.messages.push({ role: 'assistant', content: assistantContent }); assistantContent = ''; }
73
- const argsSummary = JSON.stringify(block.input || {}).slice(0, 60);
74
- chatState.messages.push({ role: 'tool', name: block.name, argsSummary, content: JSON.stringify(block.input || {}, null, 2) });
75
- }
76
- }
77
- } else if (role === 'tool') {
78
- const tc = Array.isArray(data.content) ? data.content[0] : data;
79
- chatState.messages.push({ role: 'tool', name: 'result', argsSummary: '', content: String(tc?.content || tc?.text || JSON.stringify(tc)) });
80
- }
81
- }
82
- if (event === 'done' && data.result) { if (!assistantContent) assistantContent = data.result; }
83
- if (event === 'error') assistantContent = 'error: ' + (data.error || 'unknown');
84
- }
66
+ const events = await fetchChatEvents(trimmed, chatState, renderPage);
67
+ const assistantContent = applyChatEvents(events, chatState, syncMessages);
85
68
  if (assistantContent) chatState.messages.push({ role: 'assistant', content: assistantContent });
86
69
  if (!events.length) chatState.messages.push({ role: 'assistant', content: '(no response)' });
87
70
  } catch (e) {
88
- chatState.messages.push({ role: 'assistant', content: 'error: ' + e.message });
71
+ if (e.name === 'AbortError') chatState.messages.push({ role: 'assistant', content: '[cancelled]' });
72
+ else chatState.messages.push({ role: 'assistant', content: 'error: ' + e.message });
89
73
  }
74
+ chatState.abort = null;
90
75
  chatState.busy = false;
91
- renderChatMessages(getMsgsContainer(), chatState.messages);
76
+ chatState.progress = '';
77
+ syncMessages();
78
+ renderPage();
92
79
  };
93
80
 
94
81
  const recentPaths = getRecentPaths();
95
82
  const datalistId = 'fd-cwd-list';
96
83
  const byCat = skills.reduce((a, s) => { const c = s.category || 'other'; (a[c] = a[c] || []).push(s); return a; }, {});
97
84
 
98
- setTimeout(() => renderChatMessages(getMsgsContainer(), chatState.messages), 50);
85
+ const renderPage = () => {
86
+ const host = getChatHost();
87
+ if (host) {
88
+ host.busy = chatState.busy;
89
+ host.placeholder = chatState.busy
90
+ ? (chatState.progress || 'agent working…')
91
+ : 'describe what you want to do in the working directory…';
92
+ }
93
+ // Refresh disabled state on header buttons.
94
+ const newBtn = root.querySelector('.fd-chat-new');
95
+ if (newBtn) newBtn.disabled = !!chatState.busy;
96
+ const cancelBtn = root.querySelector('.fd-chat-cancel');
97
+ if (cancelBtn) cancelBtn.style.display = chatState.busy ? '' : 'none';
98
+ };
99
+
100
+ // After mount, seed messages onto the ds-chat element + wire the send event.
101
+ setTimeout(() => {
102
+ const host = getChatHost();
103
+ if (host && !host._fdBound) {
104
+ host._fdBound = true;
105
+ host.addEventListener('send', (e) => { sendChat(e.detail && e.detail.text); });
106
+ host.messages = chatState.messages.map(toKitMessage);
107
+ host.placeholder = 'describe what you want to do in the working directory…';
108
+ host.sub = chatState.sessionId ? ('session ' + chatState.sessionId.slice(0, 8)) : 'agent';
109
+ }
110
+ renderPage();
111
+ }, 50);
99
112
 
100
113
  const selSkill = h('select', { name: 'skill', onchange: (ev) => { chatState.skill = ev.target.value; } },
101
- h('option', { value: '' }, 'no skill'),
114
+ h('option', { value: '' }, 'no skill'),
102
115
  ...Object.entries(byCat).map(([cat, ss]) =>
103
116
  h('optgroup', { label: cat },
104
117
  ...ss.map(s => h('option', { value: s.name, selected: chatState.skill === s.name ? 'true' : null, title: s.description || s.name }, skillLabel(s)))
105
118
  )));
106
119
 
107
120
  const selProv = h('select', { name: 'provider', onchange: (ev) => { chatState.provider = ev.target.value; } },
108
- h('option', { value: '' }, configuredProviders.length ? 'auto' : 'no providers configured'),
109
- ...configuredProviders.map(p => h('option', { value: p.name, selected: chatState.provider === p.name ? 'true' : null }, (p.available ? ' ' : ' ') + p.name)));
121
+ h('option', { value: '' }, configuredProviders.length ? 'auto' : 'no providers configured'),
122
+ ...configuredProviders.map(p => h('option', { value: p.name, selected: chatState.provider === p.name ? 'true' : null }, (p.available ? '(on) ' : '(off) ') + p.name)));
110
123
 
111
124
  return [
112
125
  Panel({
113
126
  title: 'chat',
114
- right: h('button', { class: 'btn-primary fd-btn-mini', onclick: (ev) => { ev.preventDefault(); newSession(); }, disabled: chatState.busy ? 'true' : null }, '+ new session'),
127
+ right: h('div', { class: 'fd-chat-actions' },
128
+ h('button', { class: 'btn-secondary fd-btn-mini fd-chat-cancel', style: chatState.busy ? '' : 'display:none', onclick: (ev) => { ev.preventDefault(); cancelInFlight(); } }, 'cancel'),
129
+ h('button', { class: 'btn-primary fd-btn-mini fd-chat-new', onclick: (ev) => { ev.preventDefault(); newSession(); }, disabled: chatState.busy ? 'true' : null }, 'new session')
130
+ ),
115
131
  children: [
116
132
  h('datalist', { id: datalistId }, ...recentPaths.map(p => h('option', { value: p }))),
117
- h('form', { class: 'row-form fd-chat-form', onsubmit: sendChat },
118
- h('div', { class: 'fd-chat-field' },
133
+ h('div', { class: 'fd-chat-config' },
134
+ h('div', { class: 'fd-chat-field fd-chat-field-grow' },
119
135
  h('label', {}, 'working directory'),
120
136
  h('input', { name: 'cwd', type: 'text', placeholder: 'e.g. C:/dev/myproject or /home/user/project', value: chatState.cwd, list: datalistId, oninput: (ev) => { chatState.cwd = ev.target.value; } })),
121
137
  h('div', { class: 'fd-chat-row' },
122
138
  h('div', { class: 'fd-chat-field fd-chat-field-grow' }, h('label', {}, 'skill'), selSkill),
123
139
  h('div', { class: 'fd-chat-field fd-chat-field-grow' }, h('label', {}, 'provider'), selProv),
124
140
  h('div', { class: 'fd-chat-field fd-chat-field-grow' }, h('label', {}, 'model (optional)'),
125
- h('input', { name: 'model', type: 'text', placeholder: configuredProviders.find(p => p.name === chatState.provider)?.defaultModel || 'default', value: chatState.model, oninput: (ev) => { chatState.model = ev.target.value; } }))),
126
- h('div', { class: 'fd-chat-submit' },
127
- h('textarea', { name: 'prompt', placeholder: 'describe what you want to do in the working directory…', rows: 4,
128
- oninput: (ev) => { ev.target.style.height = 'auto'; ev.target.style.height = Math.min(ev.target.scrollHeight, 240) + 'px'; } }),
129
- h('button', { type: 'submit', class: 'btn-primary', disabled: chatState.busy ? 'true' : null }, chatState.busy ? '…' : 'send'))),
130
- h('div', { id: 'fd-chat-msgs', class: 'fd-chatlog' }),
141
+ h('input', { name: 'model', type: 'text', placeholder: configuredProviders.find(p => p.name === chatState.provider)?.defaultModel || 'default', value: chatState.model, oninput: (ev) => { chatState.model = ev.target.value; } })))),
142
+ // Live chat surface — the kit's ds-chat web component handles
143
+ // layout, scroll, empty state, bubble chrome, tool-call cards,
144
+ // composer focus rings, send/cancel button swap, etc.
145
+ h('ds-chat', { class: 'fd-dashboard-chat ds-247420', title: 'chat', placeholder: 'describe what you want to do in the working directory…' }),
131
146
  ],
132
147
  }),
133
148
  configuredProviders.length === 0
134
149
  ? Panel({ title: 'no providers configured', children: Receipt({ rows: [
135
150
  ['set API key', 'go to keys tab, click a provider chip to set its key'],
136
151
  ['then reload', 'refresh this page to see providers here'],
137
- ['or use acptoapi', 'run acptoapi server on localhost:4800 for local LLMs'],
152
+ ['or use a gateway', 'run a gateway server on localhost:4800 for local LLMs'],
138
153
  ] }) })
139
154
  : Panel({ title: 'configured providers', children: h('div', { class: 'fd-chip-wrap' },
140
- ...providers.map(p => Chip({ tone: p.configured ? (p.available ? 'ok' : 'warn') : 'miss', children: p.name + (p.configured ? (p.available ? '' : '') : '') }))) }),
155
+ ...providers.map(p => Chip({ tone: p.configured ? (p.available ? 'ok' : 'warn') : 'miss', children: p.configured ? [p.name, ' ', p.available ? Icon('circle-dot') : Icon('circle')] : p.name }))) }),
141
156
  ];
142
157
  };
143
158
  }