@3sln/trove 0.0.2

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 (162) hide show
  1. package/README.md +1227 -0
  2. package/package.json +75 -0
  3. package/packages/core/src/collections/index.js +249 -0
  4. package/packages/core/src/errors.js +186 -0
  5. package/packages/core/src/identity/discovery.js +210 -0
  6. package/packages/core/src/identity/index.js +188 -0
  7. package/packages/core/src/identity/jwt.js +199 -0
  8. package/packages/core/src/index.js +104 -0
  9. package/packages/core/src/indexers/contribution.js +115 -0
  10. package/packages/core/src/indexers/registry.js +162 -0
  11. package/packages/core/src/indexing.js +340 -0
  12. package/packages/core/src/issues.js +150 -0
  13. package/packages/core/src/kv.js +0 -0
  14. package/packages/core/src/links.js +141 -0
  15. package/packages/core/src/metadata/cursor.js +73 -0
  16. package/packages/core/src/metadata/interface.js +244 -0
  17. package/packages/core/src/metadata/memory.js +270 -0
  18. package/packages/core/src/metadata/sqlite.js +412 -0
  19. package/packages/core/src/notifications/index.js +139 -0
  20. package/packages/core/src/notifications/webpush.js +217 -0
  21. package/packages/core/src/plugins/contributions.js +177 -0
  22. package/packages/core/src/plugins/identity.js +98 -0
  23. package/packages/core/src/plugins/index.js +225 -0
  24. package/packages/core/src/plugins/indexers.js +142 -0
  25. package/packages/core/src/plugins/installStore.js +134 -0
  26. package/packages/core/src/plugins/package.js +102 -0
  27. package/packages/core/src/plugins/packageStore.js +61 -0
  28. package/packages/core/src/plugins/runtime.js +101 -0
  29. package/packages/core/src/plugins/sql.js +52 -0
  30. package/packages/core/src/retry.js +74 -0
  31. package/packages/core/src/scan.js +302 -0
  32. package/packages/core/src/search/embeddings.js +128 -0
  33. package/packages/core/src/search/index.js +200 -0
  34. package/packages/core/src/search/keywordStore.js +107 -0
  35. package/packages/core/src/search/sqliteStores.js +455 -0
  36. package/packages/core/src/search/tagMatch.js +59 -0
  37. package/packages/core/src/search/transformer.js +195 -0
  38. package/packages/core/src/search/vectorStore.js +274 -0
  39. package/packages/core/src/search/vectorize.js +249 -0
  40. package/packages/core/src/sidecar/document.js +213 -0
  41. package/packages/core/src/sidecar/index.js +174 -0
  42. package/packages/core/src/sidecar/manager.js +239 -0
  43. package/packages/core/src/sidecar/store.js +46 -0
  44. package/packages/core/src/signedUrls.js +170 -0
  45. package/packages/core/src/sqlite-d1.js +162 -0
  46. package/packages/core/src/sqlite-driver.js +42 -0
  47. package/packages/core/src/sqlite.js +162 -0
  48. package/packages/core/src/storage/filesystem.js +283 -0
  49. package/packages/core/src/storage/interface.js +222 -0
  50. package/packages/core/src/storage/memory.js +113 -0
  51. package/packages/core/src/storage/prefixed.js +75 -0
  52. package/packages/core/src/storage/s3.js +316 -0
  53. package/packages/core/src/storage/s3sigv4.js +185 -0
  54. package/packages/core/src/tasks.js +228 -0
  55. package/packages/core/src/uploads.js +386 -0
  56. package/packages/core/src/util.js +125 -0
  57. package/packages/core/src/vfs.js +666 -0
  58. package/packages/plugin-sdk/src/browser.js +316 -0
  59. package/packages/plugin-sdk/src/index.js +32 -0
  60. package/packages/plugin-sdk/src/protocol.js +59 -0
  61. package/packages/plugin-sdk/src/rpc.js +95 -0
  62. package/packages/server/src/adapters/bun.js +78 -0
  63. package/packages/server/src/adapters/node.js +115 -0
  64. package/packages/server/src/adapters/staticAssets.js +123 -0
  65. package/packages/server/src/adapters/webDist.js +70 -0
  66. package/packages/server/src/adapters/worker-tasks.js +206 -0
  67. package/packages/server/src/adapters/worker.js +159 -0
  68. package/packages/server/src/cachePolicy.js +34 -0
  69. package/packages/server/src/engine/README.md +88 -0
  70. package/packages/server/src/engine/actions/scanCollection.js +114 -0
  71. package/packages/server/src/engine/index.js +95 -0
  72. package/packages/server/src/engine/lazy.js +25 -0
  73. package/packages/server/src/engine/providers/access.js +363 -0
  74. package/packages/server/src/engine/providers/core.js +405 -0
  75. package/packages/server/src/engine/providers/scan.js +67 -0
  76. package/packages/server/src/index.js +698 -0
  77. package/packages/server/src/manifest.js +98 -0
  78. package/packages/server/src/mcp/auth.js +40 -0
  79. package/packages/server/src/mcp/index.js +213 -0
  80. package/packages/server/src/mcp/protocol.js +181 -0
  81. package/packages/server/src/mcp/tools.js +351 -0
  82. package/packages/server/src/router.js +229 -0
  83. package/packages/server/src/routes.js +1066 -0
  84. package/packages/server/src/scope.js +43 -0
  85. package/packages/web/dist/assets/chunk-4xqbzebh.js +5 -0
  86. package/packages/web/dist/assets/chunk-4xqbzebh.js.map +9 -0
  87. package/packages/web/dist/assets/chunk-h05bxfbs.js +5 -0
  88. package/packages/web/dist/assets/chunk-h05bxfbs.js.map +10 -0
  89. package/packages/web/dist/assets/main-4cxs7prw.js +356 -0
  90. package/packages/web/dist/assets/main-4cxs7prw.js.map +103 -0
  91. package/packages/web/dist/assets/styles-kcx1x337.css +1 -0
  92. package/packages/web/dist/icon.svg +11 -0
  93. package/packages/web/dist/index.html +16 -0
  94. package/packages/web/dist/sql-wasm.wasm +0 -0
  95. package/packages/web/dist/sw.js +186 -0
  96. package/packages/web/src/bl/actions.js +410 -0
  97. package/packages/web/src/bl/activity.js +306 -0
  98. package/packages/web/src/bl/commands.js +274 -0
  99. package/packages/web/src/bl/fileType.js +49 -0
  100. package/packages/web/src/bl/index.js +70 -0
  101. package/packages/web/src/bl/links.js +54 -0
  102. package/packages/web/src/bl/offline.js +268 -0
  103. package/packages/web/src/bl/openers.js +71 -0
  104. package/packages/web/src/bl/pluginInstall.js +59 -0
  105. package/packages/web/src/bl/services.js +143 -0
  106. package/packages/web/src/bl/social.js +234 -0
  107. package/packages/web/src/bl/tagQuery.js +44 -0
  108. package/packages/web/src/main.js +10 -0
  109. package/packages/web/src/platform/api.js +529 -0
  110. package/packages/web/src/platform/commands.js +89 -0
  111. package/packages/web/src/platform/context.js +77 -0
  112. package/packages/web/src/platform/contributions.js +156 -0
  113. package/packages/web/src/platform/index.js +150 -0
  114. package/packages/web/src/platform/keybindings.js +199 -0
  115. package/packages/web/src/platform/mediaUrls.js +137 -0
  116. package/packages/web/src/platform/navigation.js +131 -0
  117. package/packages/web/src/platform/notifications.js +50 -0
  118. package/packages/web/src/platform/overlay.js +81 -0
  119. package/packages/web/src/platform/pluginClientDb.js +132 -0
  120. package/packages/web/src/platform/pluginDock.js +141 -0
  121. package/packages/web/src/platform/pluginFrames.js +194 -0
  122. package/packages/web/src/platform/pluginHost.js +648 -0
  123. package/packages/web/src/platform/pluginMedia.js +62 -0
  124. package/packages/web/src/platform/pluginModules.js +90 -0
  125. package/packages/web/src/platform/pluginNet.js +71 -0
  126. package/packages/web/src/platform/pluginPackage.js +247 -0
  127. package/packages/web/src/platform/pluginRpc.js +377 -0
  128. package/packages/web/src/platform/pluginSigning.js +168 -0
  129. package/packages/web/src/platform/pluginStore.js +67 -0
  130. package/packages/web/src/platform/settings.js +101 -0
  131. package/packages/web/src/platform/spatialNav.js +286 -0
  132. package/packages/web/src/platform/viewport.js +123 -0
  133. package/packages/web/src/platform/voice.js +133 -0
  134. package/packages/web/src/platform/voiceSearch.js +155 -0
  135. package/packages/web/src/platform/whenclause.js +162 -0
  136. package/packages/web/src/platform/workbench.js +156 -0
  137. package/packages/web/src/runtime.js +73 -0
  138. package/packages/web/src/styles.css +1382 -0
  139. package/packages/web/src/ui/components/activityBar.js +35 -0
  140. package/packages/web/src/ui/components/activityPanel.js +132 -0
  141. package/packages/web/src/ui/components/commandPalette.js +154 -0
  142. package/packages/web/src/ui/components/editorArea.js +75 -0
  143. package/packages/web/src/ui/components/launcher.js +392 -0
  144. package/packages/web/src/ui/components/openers/index.js +212 -0
  145. package/packages/web/src/ui/components/openers/markdown.js +222 -0
  146. package/packages/web/src/ui/components/overlays.js +255 -0
  147. package/packages/web/src/ui/components/phoneChrome.js +188 -0
  148. package/packages/web/src/ui/components/pluginReview.js +151 -0
  149. package/packages/web/src/ui/components/pluginsView.js +120 -0
  150. package/packages/web/src/ui/components/settingsView.js +258 -0
  151. package/packages/web/src/ui/components/social.js +290 -0
  152. package/packages/web/src/ui/components/statusBar.js +198 -0
  153. package/packages/web/src/ui/components/views/grid.js +115 -0
  154. package/packages/web/src/ui/components/views/index.js +155 -0
  155. package/packages/web/src/ui/components/views/list.js +50 -0
  156. package/packages/web/src/ui/components/views/parts.js +58 -0
  157. package/packages/web/src/ui/compositions/workbench.js +125 -0
  158. package/packages/web/src/ui/format.js +33 -0
  159. package/packages/web/src/ui/icon.js +81 -0
  160. package/packages/web/src/ui/media.js +114 -0
  161. package/packages/web/src/ui/sanitize.js +86 -0
  162. package/packages/web/src/workbench.js +205 -0
@@ -0,0 +1,35 @@
1
+ // The left rail — the app's only global chrome now that the title bar is gone.
2
+ // Brand mark at the top (→ home), the primary views below, then a bottom cluster
3
+ // of notifications, settings, and the signed-in principal. Search is reached from
4
+ // the Home view itself and via the double-shift modal, so there is no omni bar.
5
+
6
+ import { dd } from '../../runtime.js';
7
+ import { icon } from '../icon.js';
8
+ import { notificationBell, principalChip } from './social.js';
9
+
10
+ const { div, button, span, img } = dd;
11
+
12
+ const ITEMS = [
13
+ { id: 'home', icon: 'search', title: 'Search & files', command: 'workbench.view.home' },
14
+ { id: 'plugins', icon: 'plug', title: 'Plugins', command: 'workbench.view.plugins' },
15
+ ];
16
+
17
+ export default function activityBar(state, ui) {
18
+ const active = state.wb.activity;
19
+ const pluginCount = state.plugins?.filter((p) => p.status === 'active').length || 0;
20
+ return div({ className: 'activitybar' },
21
+ button({ className: 'brand-mark', title: 'Trove — home' }, img({ src: '/icon.svg', alt: 'Trove' }))
22
+ .on({ click: () => ui.exec('workbench.view.home') }),
23
+ ...ITEMS.map((it) =>
24
+ button({ className: `item ${active === it.id ? 'active' : ''}`, title: it.title },
25
+ icon(it.icon, { size: 22 }),
26
+ it.id === 'plugins' && pluginCount ? span({ className: 'badge' }, String(pluginCount)) : null,
27
+ ).on({ click: () => ui.exec(it.command) }),
28
+ ),
29
+ div({ className: 'spacer' }),
30
+ notificationBell(state, ui),
31
+ button({ className: `item ${active === 'settings' ? 'active' : ''}`, title: 'Settings' }, icon('gear', { size: 21 }))
32
+ .on({ click: () => ui.exec('workbench.openSettings') }),
33
+ principalChip(state),
34
+ );
35
+ }
@@ -0,0 +1,132 @@
1
+ // The Activity panel — what is running, and what is stuck.
2
+ //
3
+ // Two sections, because they are two different kinds of fact and the user acts on them
4
+ // differently. Running work is watched and possibly cancelled; a standing problem is
5
+ // retried or dismissed. Merging them into one "notifications" list would lose that:
6
+ // a task that finished is over, while an issue that is listed is still true right now.
7
+ //
8
+ // A task that doesn't know its total renders as an indeterminate bar rather than a
9
+ // guess. Nothing here fabricates a percentage.
10
+
11
+ import { dd } from '../../runtime.js';
12
+ import { icon } from '../icon.js';
13
+ import { bytes } from '../format.js';
14
+
15
+ const { div, button, span, h3, p } = dd;
16
+
17
+ const STATUS_ICON = { running: 'refresh', done: 'check', failed: 'close', cancelled: 'close' };
18
+
19
+ function amount(task) {
20
+ if (task.total == null) return null;
21
+ return task.unit === 'bytes'
22
+ ? `${bytes(task.done || 0)} of ${bytes(task.total)}`
23
+ : `${task.done || 0} of ${task.total}${task.unit ? ` ${task.unit}` : ''}`;
24
+ }
25
+
26
+ function taskRow(task, ui) {
27
+ const running = task.status === 'running';
28
+ const determinate = running && task.total != null && task.total > 0;
29
+ const pct = determinate ? Math.min(100, Math.round(((task.done || 0) / task.total) * 100)) : 0;
30
+ return div({ className: `act-task act-${task.status}`, 'data-task-id': task.id },
31
+ div({ className: 'act-row' },
32
+ running
33
+ ? div({ className: 'spinner', $styling: { width: '12px', height: '12px' } })
34
+ : icon(STATUS_ICON[task.status] || 'info', { size: 13 }),
35
+ div({ className: 'act-body' },
36
+ div({ className: 'act-title' }, task.title),
37
+ task.detail ? div({ className: 'act-detail' }, task.detail) : null,
38
+ task.error ? div({ className: 'act-error' }, task.error) : null,
39
+ ),
40
+ running && task.cancellable
41
+ ? button({ className: 'act-action', title: 'Cancel' }, icon('close', { size: 12 })).on({ click: () => ui.app.activity.cancel(task.id) })
42
+ : !running
43
+ ? button({ className: 'act-action', title: 'Dismiss' }, icon('close', { size: 12 })).on({ click: () => ui.app.activity.dismiss(task.id) })
44
+ : null,
45
+ ),
46
+ running
47
+ // An indeterminate bar animates without claiming a position. The alternative —
48
+ // picking a number — would be a lie dressed as information.
49
+ ? div({ className: `act-bar ${determinate ? '' : 'indeterminate'}` },
50
+ div({ className: 'act-fill', $styling: determinate ? { width: `${pct}%` } : {} }))
51
+ : null,
52
+ running && amount(task) ? div({ className: 'act-amount' }, amount(task)) : null,
53
+ );
54
+ }
55
+
56
+ function issueRow(issue, ui) {
57
+ const since = new Date(issue.firstAt || issue.lastAt).toLocaleString();
58
+ return div({ className: `act-issue act-sev-${issue.severity || 'error'}`, 'data-issue-id': issue.id },
59
+ div({ className: 'act-row' },
60
+ icon(issue.severity === 'warning' ? 'info' : 'close', { size: 13 }),
61
+ div({ className: 'act-body' },
62
+ div({ className: 'act-title' }, issue.title),
63
+ issue.detail ? div({ className: 'act-detail' }, issue.detail) : null,
64
+ div({ className: 'act-meta' }, issue.count > 1 ? `${issue.count} times, since ${since}` : `since ${since}`),
65
+ ),
66
+ ),
67
+ div({ className: 'act-actions' },
68
+ // Retry only when the server said pressing it will actually do something.
69
+ issue.retryable
70
+ ? button({ className: 'btn small act-retry' }, icon('refresh', { size: 12 }), span('Retry'))
71
+ .on({ click: () => ui.app.activity.retryIssue(issue.id) })
72
+ : null,
73
+ button({ className: 'btn small ghost act-dismiss' }, 'Dismiss')
74
+ .on({ click: () => ui.app.activity.dismissIssue(issue.id) }),
75
+ ),
76
+ );
77
+ }
78
+
79
+ export default function activityPanel(state, ui) {
80
+ const act = state.act || { tasks: [], issues: [] };
81
+ if (!act.open) return null;
82
+ const running = act.tasks.filter((t) => t.status === 'running');
83
+ const recent = act.tasks.filter((t) => t.status !== 'running');
84
+
85
+ return div({ className: 'activity-panel' },
86
+ div({ className: 'act-head' },
87
+ h3('Activity'),
88
+ button({ className: 'act-action', title: 'Close' }, icon('close', { size: 13 }))
89
+ .on({ click: () => ui.app.activity.togglePanel(false) }),
90
+ ),
91
+
92
+ // "Nothing is wrong" and "we couldn't ask" look identical unless one says so. Both
93
+ // loads are reported, because either can fail alone.
94
+ act.tasksError || act.issuesError
95
+ ? div({ className: 'act-offline' }, icon('info', { size: 12 }),
96
+ span(`Couldn't reach the server — this list may be out of date (${act.issuesError || act.tasksError})`))
97
+ : null,
98
+
99
+ div({ className: 'act-section' },
100
+ div({ className: 'act-section-title' }, 'Running'),
101
+ running.length
102
+ ? div(...running.map((t) => taskRow(t, ui)))
103
+ : p({ className: 'act-empty' }, 'Nothing running.'),
104
+ ),
105
+
106
+ act.issues.length
107
+ ? div({ className: 'act-section' },
108
+ div({ className: 'act-section-title' }, `Needs attention (${act.issues.length})`),
109
+ div(...act.issues.map((i) => issueRow(i, ui))),
110
+ )
111
+ : div({ className: 'act-section' },
112
+ div({ className: 'act-section-title' }, 'Needs attention'),
113
+ // An unreadable list is not an empty one, and must never render as "all clear".
114
+ p({ className: 'act-empty' },
115
+ act.issuesError ? 'Could not load the list of problems.' : act.issuesLoading ? 'Checking…' : 'No standing problems.'),
116
+ ),
117
+
118
+ recent.length
119
+ ? div({ className: 'act-section' },
120
+ div({ className: 'act-section-title' }, 'Recently finished'),
121
+ div(...recent.map((t) => taskRow(t, ui))),
122
+ )
123
+ : null,
124
+
125
+ div({ className: 'act-foot act-actions' },
126
+ button({ className: 'btn small ghost act-rebuild' }, icon('refresh', { size: 12 }), span('Rebuild search index'))
127
+ .on({ click: () => ui.exec('workbench.rebuildIndex') }),
128
+ button({ className: 'btn small ghost act-scan' }, icon('refresh', { size: 12 }), span('Scan for outside changes'))
129
+ .on({ click: () => ui.exec('workbench.scanCollection') }),
130
+ ),
131
+ );
132
+ }
@@ -0,0 +1,154 @@
1
+ // Command palette + quick-open. Mode 'commands' fuzzy-filters registered
2
+ // commands (respecting when-clauses); mode 'files' searches the drive by name
3
+ // and opens the chosen file. Arrow keys move, Enter runs, Esc closes.
4
+
5
+ import { dd } from '../../runtime.js';
6
+ import { icon, iconForNode } from '../icon.js';
7
+ import { prettyKey } from '../../platform/keybindings.js';
8
+ import { OpenFileAction, QuickOpenAction } from '../../bl/actions.js';
9
+
10
+ const { div, input, span } = dd;
11
+
12
+ export default function commandPalette(state, ui) {
13
+ const pal = state.overlay.palette;
14
+ if (!pal) return null;
15
+ const wb = ui.platform.workbench;
16
+ // With no query there is nothing to show — and showing the PREVIOUS session's hits
17
+ // would be worse than nothing, since they don't match what the field now says.
18
+ const items = pal.mode === 'files'
19
+ ? (pal.query.trim() ? state.se.paletteFiles || [] : [])
20
+ : filterCommands(state, ui, pal.query);
21
+ const index = Math.min(pal.index, Math.max(0, items.length - 1));
22
+
23
+ const run = (item) => {
24
+ if (!item) return;
25
+ wb.closePalette();
26
+ if (pal.mode === 'files') ui.go(new OpenFileAction(item.node));
27
+ else ui.exec(item.id);
28
+ };
29
+
30
+ return div({},
31
+ div({ className: 'scrim' }).on({ click: () => wb.closePalette() }),
32
+ div({ className: 'palette' },
33
+ div({ className: 'search' },
34
+ icon(pal.mode === 'files' ? 'search' : 'command', { size: 18 }),
35
+ input({
36
+ type: 'text', value: pal.query, autofocus: true,
37
+ placeholder: pal.mode === 'files' ? 'Search files by name…' : 'Type a command…',
38
+ }).on({
39
+ // NOTE: the mode is read from live state inside onQuery, NOT captured here.
40
+ // A listener belongs to the render that created it, and rendering is async —
41
+ // a keystroke landing between `openPalette('files')` and the re-render would
42
+ // otherwise be handled as a COMMANDS keystroke and silently do nothing.
43
+ input: (e) => onQuery(ui, e.target.value),
44
+ keydown: (e) => onKey(e, ui, items, index, run),
45
+ $attach: (el) => {
46
+ queueMicrotask(() => el.focus());
47
+ // Same reason in reverse: dodo only patches `value` when the vnode prop
48
+ // changed, so a reopened palette whose query is '' both times would keep
49
+ // whatever text the previous session left in the live DOM node.
50
+ if (el.value !== pal.query) el.value = pal.query;
51
+ },
52
+ }),
53
+ ),
54
+ div({ className: 'results' },
55
+ items.length
56
+ ? items.map((item, i) => {
57
+ const hover = () => wb.setPaletteIndex(i);
58
+ return pal.mode === 'files' ? fileOpt(item, i === index, run, hover) : cmdOpt(item, i === index, ui, run, hover);
59
+ })
60
+ : emptyResults(pal, state),
61
+ ),
62
+ ),
63
+ );
64
+ }
65
+
66
+ // Nothing to show is three different situations, and saying "No files found" for all
67
+ // three tells the user something false in two of them.
68
+ function emptyResults(pal, state) {
69
+ if (pal.mode !== 'files') return div({ className: 'none' }, 'No matching commands');
70
+ if (state.se.paletteError) return div({ className: 'none error' }, icon('warn', { size: 14 }), ` ${state.se.paletteError}`);
71
+ if (state.se.paletteLoading) return div({ className: 'none' }, div({ className: 'spinner' }), ' Searching…');
72
+ return div({ className: 'none' }, pal.query.trim() ? 'No files found' : 'Type to search files by name');
73
+ }
74
+
75
+ function cmdOpt(cmd, active, ui, run, hover) {
76
+ const key = ui.platform.keybindings.labelFor(cmd.id);
77
+ const available = ui.platform.commands.isAvailable(cmd);
78
+ return div({ className: `opt ${active ? 'active' : ''} ${available ? '' : 'unavailable'}` },
79
+ span({ className: 'ico' }, icon(cmd.icon || 'command', { size: 16 })),
80
+ cmd.category ? span({ className: 'cat' }, cmd.category + ' ›') : null,
81
+ span({ className: 'title' }, cmd.title),
82
+ !available ? span({ className: 'offline-tag' }, 'offline') : null,
83
+ key ? span({ className: 'kbd' }, dd.h('kbd', prettyKey(keyRaw(ui, cmd.id)))) : null,
84
+ ).on({ click: () => run(cmd), mouseenter: hover });
85
+ }
86
+
87
+ function keyRaw(ui, id) {
88
+ const b = ui.platform.keybindings.resolved().find((x) => x.command === id);
89
+ return b ? b.key : '';
90
+ }
91
+
92
+ function fileOpt(item, active, run, hover) {
93
+ return div({ className: `opt ${active ? 'active' : ''}` },
94
+ span({ className: 'ico' }, icon(iconForNode(item.node), { size: 16 })),
95
+ span({ className: 'title' }, item.node.name),
96
+ span({ className: 'sub' }, item.node.contentType || ''),
97
+ ).on({ click: () => run(item), mouseenter: hover });
98
+ }
99
+
100
+ function filterCommands(state, ui, query) {
101
+ const all = ui.platform.commands.paletteCommands();
102
+ const q = query.trim().toLowerCase();
103
+ if (!q) return all.slice(0, 60);
104
+ const scored = [];
105
+ for (const c of all) {
106
+ const hay = `${c.category || ''} ${c.title}`.toLowerCase();
107
+ const s = fuzzyScore(hay, q);
108
+ if (s > 0) scored.push([s, c]);
109
+ }
110
+ return scored.sort((a, b) => b[0] - a[0]).slice(0, 60).map((x) => x[1]);
111
+ }
112
+
113
+ function fuzzyScore(hay, q) {
114
+ let score = 0;
115
+ let hi = 0;
116
+ for (const ch of q) {
117
+ const idx = hay.indexOf(ch, hi);
118
+ if (idx < 0) return 0;
119
+ score += idx === hi ? 3 : 1;
120
+ hi = idx + 1;
121
+ }
122
+ if (hay.includes(q)) score += 10;
123
+ return score;
124
+ }
125
+
126
+ function onKey(e, ui, items, index, run) {
127
+ const wb = ui.platform.workbench;
128
+ if (e.key === 'ArrowDown') {
129
+ e.preventDefault();
130
+ wb.movePalette(1, items.length);
131
+ } else if (e.key === 'ArrowUp') {
132
+ e.preventDefault();
133
+ wb.movePalette(-1, items.length);
134
+ } else if (e.key === 'Enter') {
135
+ e.preventDefault();
136
+ run(items[index]);
137
+ } else if (e.key === 'Escape') {
138
+ e.preventDefault();
139
+ wb.closePalette();
140
+ }
141
+ }
142
+
143
+ let fileTimer = null;
144
+ function onQuery(ui, value) {
145
+ const wb = ui.platform.workbench;
146
+ wb.setPaletteQuery(value);
147
+ // The palette's CURRENT mode, not whichever one was on screen when this listener
148
+ // was created — see the note at the input.
149
+ clearTimeout(fileTimer);
150
+ if (wb.overlay.state.palette?.mode !== 'files') return;
151
+ // Route the file search through an action; results land in the reactive search
152
+ // service (state.se.paletteFiles), so the palette re-renders without ad-hoc state.
153
+ fileTimer = setTimeout(() => ui.go(new QuickOpenAction(value)), 200);
154
+ }
@@ -0,0 +1,75 @@
1
+ // The viewer — the top of the panel stack (a file's opener), full-width, with a
2
+ // nav bar: a Back button, a trail of the stacked panels (Search › A › B),
3
+ // and details/close actions. The opener host returns a *stable* keyed alias per
4
+ // panel so dodo reuses its DOM across unrelated workbench re-renders — critical for
5
+ // any media opener, whose <audio>/<video> must keep playing while state churns; the
6
+ // DOM persists as long as the panel stays in the stack.
7
+
8
+ import { dd } from '../../runtime.js';
9
+ import { icon, iconForNode } from '../icon.js';
10
+ import { renderOpener } from './openers/index.js';
11
+ import { availableOpeners } from '../../bl/openers.js';
12
+
13
+ const { div, span, button } = dd;
14
+
15
+ const openerFns = new Map(); // `${panelId}:${openerId}` -> stable render fn
16
+
17
+ export default function editorArea(state, ui) {
18
+ const wb = state.wb;
19
+ const files = state.nav.stack.filter((p) => p.kind === 'file');
20
+ pruneOpeners(files);
21
+ const active = files[files.length - 1];
22
+ if (!active) return div({ className: 'editor' });
23
+ return div({ className: 'editor' },
24
+ navBar(files, active, ui),
25
+ div({ className: 'stage' }, openerHost(active, ui)),
26
+ );
27
+ }
28
+
29
+ function navBar(files, active, ui) {
30
+ const w = ui.platform.workbench;
31
+ return div({ className: 'viewer-nav' },
32
+ button({ className: 'vn-back', title: 'Back (Esc)' }, icon('chevron-left', { size: 16 }))
33
+ .on({ click: () => w.back() }),
34
+ div({ className: 'vn-trail' },
35
+ button({ className: 'vn-crumb' }, icon('search', { size: 13 }), span('Search'))
36
+ .on({ click: () => w.showHome() }),
37
+ ...files.map((p) =>
38
+ button({ className: `vn-crumb ${p.id === active.id ? 'active' : ''}`, title: p.node.name },
39
+ icon(iconForNode(p.node), { size: 13 }), span({ className: 'label' }, p.node.name))
40
+ .on({ click: () => w.openFile(p.node, p.openerId) })),
41
+ ),
42
+ div({ className: 'vn-actions' },
43
+ openerSwitch(active, ui),
44
+ button({ className: 'iconbtn', title: 'Details & comments' }, icon('info', { size: 15 }))
45
+ .on({ click: () => w.toggleInfoPanel() }),
46
+ button({ className: 'iconbtn', title: 'Close (Esc)' }, icon('close', { size: 15 }))
47
+ .on({ click: () => w.showHome() }),
48
+ ),
49
+ );
50
+ }
51
+
52
+ // "Open with…" — shown only when more than one opener can handle this file. Opens
53
+ // the chooser (pre-selected to the current opener) so the user can switch viewers,
54
+ // and optionally make the choice their default for this file type.
55
+ function openerSwitch(active, ui) {
56
+ const openers = availableOpeners(ui.platform, active.node);
57
+ if (openers.length <= 1) return null;
58
+ return button({ className: 'iconbtn', title: 'Open with…' }, icon('dots', { size: 15 }))
59
+ .on({ click: () => ui.platform.workbench.showDialog({ kind: 'opener-chooser', node: active.node, openers, current: active.openerId }) });
60
+ }
61
+
62
+ function openerHost(panel, ui) {
63
+ const key = `${panel.id}:${panel.openerId}`;
64
+ let fn = openerFns.get(key);
65
+ if (!fn) {
66
+ fn = () => renderOpener(panel.node, panel.openerId, ui);
67
+ openerFns.set(key, fn);
68
+ }
69
+ return dd.alias(fn)().key(panel.id);
70
+ }
71
+
72
+ function pruneOpeners(files) {
73
+ const live = new Set(files.map((p) => `${p.id}:${p.openerId}`));
74
+ for (const k of openerFns.keys()) if (!live.has(k)) openerFns.delete(k);
75
+ }