dsh-asuta-desktop-navigation 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Asuta and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,33 @@
1
- # Temporary Holding Version
1
+ # Desktop Navigation
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Workspace-authored bundle for Windows DSH Desktop. It combines local Chat-path navigation with a narrowly changed, MIT-licensed copy of the currently installed `dsh-recent` 0.1.0 browser artifact.
4
+
5
+ ## 1.0.3 — shared sidebar scroll compatibility
6
+
7
+ When Sidebar More enables its shared browsing viewport, Recent uses that viewport for incremental loading. A layout-change event rebinds the observer when Sidebar More activates, collapses, expands or unloads; without the shared viewport it uses the original workspace list. The source patch lives in `build.mjs` and `client-extension.js`, and the vendored upstream artifact remains untouched. Verified in DSH 0.2.2-rc.1 with wheel scrolling, incremental loading, independent folding, search and cross-workspace mouse drag/undo.
8
+
9
+ Installed in the current profile as `dsh-asuta-desktop-navigation` through a local source link. The original `include:dsh-recent` row is disabled through Plugin Manager to avoid duplicate workspace-fold observers. To roll back, disable this bundle and re-enable the original row through Plugin Manager. Do not activate both copies. The package was renamed from `@local/desktop-navigation` to give the native loader a fresh client manifest identity without restarting the backend.
10
+
11
+ ## Behavior
12
+
13
+ Existing Chat blue file links retain the original StoredEntry, component inputs, child declarations, store, hooks, localization, and error/retry surface. A guarded wrapper around that entry's component changes only its `openFile` prop. Relative paths use the viewed Session's current workspace. A directory opens directly in Explorer; a file reveals its containing directory and selects the file. HTTP(S), arbitrary schemes, UNC shares, device paths, control characters, alternate data streams, and unresolved relative paths are rejected. Line anchors such as `#L24-L30` are removed from filesystem targets.
14
+
15
+ A small authenticated Host route, `POST /desktop-navigation/path-info`, performs read-only metadata checks through the composed `fs` capability and refuses absent/unsupported targets. It verifies a local Host mapping and revalidates the canonical path. The Client then calls the existing native `remote.session.openWorkspacePath` operation: directories use ordinary open and files use `action: reveal`. Paths never become shell commands. This bundle contains no subprocess launches and no alternative URL opener.
16
+
17
+ Recent retains upstream ordering, row actions, persistent options, workspace folding, paging, portals, and selectors. Double-clicking a Recent conversation title opens the native rename dialog, matching Workspace and Pinned lists. It reuses the shared native-row bridge from sidebar-more 1.0.2 when available and installs its own guarded bridge otherwise. The Recent detail card shows the session title, working directory, relative time, and native status lines. One shared card waits 800ms on the first hover, then switches immediately between rows, with a 200ms pointer grace. CSS anchor positioning keeps it beside the active row without synchronous geometry reads during each handoff; row actions use CSS hover visibility. Its own list receives a 180ms height transition and 140ms opacity transition. The header chevron rotates over 180ms. Closed content becomes inert immediately and unmounts after the transition; the paging sentinel is absent while folded. `prefers-reduced-motion: reduce` disables transitions and removes closed content immediately.
18
+
19
+ ## Compatibility and rollback
20
+
21
+ The Chat adapter uses the inspected registry APIs to observe declaration lifetimes and trigger re-rendering, plus a guarded private seam on the native Chat StoredEntry's writable component property. It preserves that entry's complete standard kit and original child ownership. The wrapper forwards every prop and changes only openFile; it does not re-declare any slot or replace the registered entry. Disposal restores the exact descriptor only while the wrapper still owns it. Activating or unloading the wrapper remounts local Chat React state once; external store and cached injected values stay attached to their original entry. Startup ACK has been observed as navigation-ready on the installed client.
22
+
23
+ The copied upstream recent implementation already includes its own DOM-based workspace-fold compatibility layer and primitive imports. Those upstream behaviors are retained; the new animation code only owns its React-rendered list box. No DSH installation/profile files were modified. The vendored original remains byte-for-byte preserved under `vendor/recent-original.js`, with attribution and MIT license. `vendor/UPSTREAM.json` records the source and SHA-256. The build refuses a different upstream fingerprint.
24
+
25
+ ## Build and verification
26
+
27
+ Run `node build.mjs` inside this directory. It builds entirely from the retained original artifact; no dependency installation is required. Run `node --test ../.desktop-navigation/navigation.test.mjs` for URL/path rejection, native call intent, absent-target checks, complete registration preservation, and loader/motion contracts. All native calls in tests are stubs. No user folder or file is opened during testing.
28
+
29
+ Final integration checks on 2026-10-26: the installed Client acknowledged navigation-ready, the native chat and trajectory entries remain active with no duplicate chat or transient marker, and sidebar.footer.action contains one active recent entry. All 12 navigation tests pass, including full factory/apply, actual wrapper render with native-call stubs, descriptor restoration, changed registry entries, and concurrent-owner protection. The combined suite has 35 passing tests plus 22 isolated settings-persistence assertions. Syntax checks and git diff --check pass.
30
+
31
+ Verification limits: animation appearance, displayed artwork, and end-user Explorer handoff were not visually/click tested in the authenticated main window. The bundle deliberately does not launch Explorer automatically as a test. No real conversation history or user model settings were changed, and no Git commit or push was performed.
32
+
33
+ Original icon: created specifically for this bundle. Original upstream recent copyright and license remain in `vendor/LICENSE.dsh-recent`.
@@ -0,0 +1,215 @@
1
+ function reportClientStage(stage, error) {
2
+ const detail = String(error?.message ?? error ?? '').slice(0, 2000);
3
+ reportClientStage.queue = (reportClientStage.queue ?? Promise.resolve()).then(async () => {
4
+ try { await fetch('/desktop-navigation/client-status', { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ stage, error: detail }) }); } catch { /* Diagnostics must never prevent UI activation. */ }
5
+ });
6
+ return reportClientStage.queue;
7
+ }
8
+
9
+ /** Keep paging relative to the actual viewport, including sidebar-more hot reloads. */
10
+ function useRecentScrollRoot(area) {
11
+ const resolve = () => area?.closest('[data-sm-scroll-region]') ?? area;
12
+ const [root, setRoot] = react.useState(resolve);
13
+ react.useLayoutEffect(() => {
14
+ const sync = () => setRoot(resolve());
15
+ sync();
16
+ window.addEventListener('sidebar-more:scroll-root-change', sync);
17
+ return () => window.removeEventListener('sidebar-more:scroll-root-change', sync);
18
+ }, [area]);
19
+ return root;
20
+ }
21
+
22
+ const SIDEBAR_MORE_RENAME_BRIDGE = Symbol.for('sidebar-more.native-row-bridge');
23
+ const SIDEBAR_MORE_ROW_WRAPPER = Symbol.for('sidebar-more.workspace-row-bridge');
24
+ const RECENT_RENAME_WRAPPER = Symbol.for('desktop-navigation.recent-rename-bridge');
25
+ let nativeRenameRequest = null;
26
+ let nativeHoverRender = null;
27
+
28
+ function requestRecentSessionRename(sessionId, title) {
29
+ const shared = window[SIDEBAR_MORE_RENAME_BRIDGE]?.getSnapshot?.()?.requestSessionRename;
30
+ (shared ?? nativeRenameRequest)?.(sessionId, title);
31
+ }
32
+
33
+ function renderRecentHoverSlot(sessionId) {
34
+ const shared = window[SIDEBAR_MORE_RENAME_BRIDGE]?.getSnapshot?.()?.renderSlot;
35
+ return (shared ?? nativeHoverRender)?.('sidebar.session.row.hover', { sessionId }) ?? null;
36
+ }
37
+
38
+ function installRecentRenameBridge(ctx) {
39
+ const slots = ctx.slots, original = Symbol.for('cordis.original');
40
+ return slots.inject('sidebar.workspaces', () => {
41
+ let patch = null, changing = false, stopped = false;
42
+ function restore() {
43
+ if (patch && patch.entry.component === patch.wrapper) Object.defineProperty(patch.entry, 'component', patch.descriptor);
44
+ patch = null;
45
+ }
46
+ function reconcile() {
47
+ if (stopped || changing) return;
48
+ changing = true;
49
+ try {
50
+ const candidate = slots.entriesOfSlot('sidebar.workspaces')[0];
51
+ const entry = candidate?.[original] ?? candidate;
52
+ if (patch && patch.entry === entry && entry?.component === patch.wrapper) return;
53
+ restore();
54
+ if (!entry || typeof entry.component !== 'function' || entry.component[SIDEBAR_MORE_ROW_WRAPPER]) return;
55
+ const descriptor = Object.getOwnPropertyDescriptor(entry, 'component');
56
+ if (!descriptor || (!descriptor.writable && !descriptor.configurable)) return;
57
+ const NativeWorkspace = entry.component;
58
+ function RecentRenameWorkspace(props) {
59
+ const request = props.requestSessionRename;
60
+ const renderSlot = props.renderSlot;
61
+ react.useLayoutEffect(() => {
62
+ nativeRenameRequest = request;
63
+ nativeHoverRender = renderSlot;
64
+ return () => {
65
+ if (nativeRenameRequest === request) nativeRenameRequest = null;
66
+ if (nativeHoverRender === renderSlot) nativeHoverRender = null;
67
+ };
68
+ }, [request, renderSlot]);
69
+ return react.createElement(NativeWorkspace, props);
70
+ }
71
+ Object.defineProperty(RecentRenameWorkspace, RECENT_RENAME_WRAPPER, { value: true });
72
+ Object.defineProperty(entry, 'component', { ...descriptor, value: RecentRenameWorkspace });
73
+ patch = { entry, descriptor, wrapper: RecentRenameWorkspace };
74
+ } catch (error) { console.warn('desktop-navigation: recent rename bridge:', error); }
75
+ finally { changing = false; }
76
+ }
77
+ const unsubscribe = slots.subscribe('sidebar.workspaces', reconcile);
78
+ reconcile();
79
+ return () => { stopped = true; unsubscribe(); nativeRenameRequest = null; nativeHoverRender = null; restore(); };
80
+ });
81
+ }
82
+
83
+ function installNavigation(ctx) {
84
+ ctx.effect(() => {
85
+ const tag = document.createElement('style');
86
+ tag.dataset.pluginCss = 'dsh-asuta-desktop-navigation/recent-layout.css';
87
+ tag.textContent = '[data-slot="sidebar.recent.section"] > [class*="_header"] { box-sizing: border-box; padding-left: var(--dsh-sidebar-row-padding, 12px); padding-right: 1px; }';
88
+ tag.textContent += '.desktop-navigation-recent-anchor{position:relative;display:block}.aalpzW_row .aalpzW_rowActions{display:none}.aalpzW_row:hover .aalpzW_rowActions,.aalpzW_rowActive .aalpzW_rowActions{display:inline-flex}.aalpzW_row:hover .aalpzW_time,.aalpzW_row:hover .aalpzW_pinIndicator,.aalpzW_rowActive .aalpzW_time,.aalpzW_rowActive .aalpzW_pinIndicator{display:none}';
89
+ document.head.appendChild(tag);
90
+ return () => { tag.remove(); };
91
+ }, 'desktop-navigation: align recent section header');
92
+ ctx.effect(() => installRecentRenameBridge(ctx), 'desktop-navigation: native recent rename bridge');
93
+ installChatNavigation(ctx, (sessionId) => createNativeOpener({
94
+ cwd: () => ctx.get('sessions')?.list?.getSnapshot()?.byId?.[sessionId]?.cwd,
95
+ remote: { openWorkspacePath(request) {
96
+ const api = ctx.get('remote')?.session;
97
+ if (typeof api?.openWorkspacePath !== 'function') throw new Error('native-unavailable');
98
+ return api.openWorkspacePath(request);
99
+ } },
100
+ available: async () => (await ctx.get('remote')?.session?.canOpenWorkspacePath?.()) === true,
101
+ inspectPath: async (path) => {
102
+ const response = await fetch('/desktop-navigation/path-info', { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path }) });
103
+ const result = await response.json();
104
+ if (!response.ok) throw new Error(result.error ?? 'invalid-path');
105
+ return result;
106
+ },
107
+ }), message => reportClientStage('navigation-render-error', message), react);
108
+ }
109
+
110
+ const RecentHoverContext = react.createContext(null);
111
+
112
+ /** One card for the Recent list. CSS anchors position it without synchronous layout reads on every handoff. */
113
+ function RecentMotionList({ folded, rows, now, t, className, children }) {
114
+ const [mounted, setMounted] = react.useState(!folded);
115
+ const [shown, setShown] = react.useState(!folded);
116
+ const [card, setCard] = react.useState(null);
117
+ const [copied, setCopied] = react.useState(false);
118
+ const [hover] = react.useState(() => {
119
+ let row = null;
120
+ let anchor = null;
121
+ let dwell = null;
122
+ let grace = null;
123
+ let warm = false;
124
+ const clearDwell = () => { clearTimeout(dwell); dwell = null; };
125
+ const clearGrace = () => { clearTimeout(grace); grace = null; };
126
+ const dismiss = () => {
127
+ clearDwell(); clearGrace();
128
+ anchor?.style.removeProperty('anchor-name');
129
+ anchor = null; row = null; warm = false;
130
+ setCard(null); setCopied(false);
131
+ };
132
+ const armGrace = () => {
133
+ clearGrace();
134
+ grace = setTimeout(dismiss, 200);
135
+ };
136
+ return {
137
+ enter(next, t, now, element) {
138
+ clearGrace();
139
+ if (row?.id === next.id && anchor === element) return;
140
+ clearDwell();
141
+ anchor?.style.removeProperty('anchor-name');
142
+ row = next; anchor = element;
143
+ anchor.style.setProperty('anchor-name', '--dsh-recent-hover-anchor');
144
+ setCopied(false);
145
+ if (warm) setCard({ row: next, t, now });
146
+ else dwell = setTimeout(() => {
147
+ dwell = null; warm = true;
148
+ setCard({ row: next, t, now });
149
+ }, CARD_DWELL_MS);
150
+ },
151
+ leave(id) {
152
+ if (row?.id !== id) return;
153
+ if (warm) armGrace();
154
+ else {
155
+ clearDwell();
156
+ anchor?.style.removeProperty('anchor-name');
157
+ anchor = null; row = null;
158
+ }
159
+ },
160
+ enterCard: clearGrace,
161
+ leaveCard: armGrace,
162
+ currentId: () => row?.id,
163
+ refresh(rows, t, now) {
164
+ if (!warm || row === null) return;
165
+ const latest = rows.find(item => item.id === row.id);
166
+ if (!latest) { dismiss(); return; }
167
+ row = latest;
168
+ setCard(previous => previous && (previous.row !== latest || previous.now !== now || previous.t !== t)
169
+ ? { row: latest, t, now } : previous);
170
+ },
171
+ dismiss,
172
+ dispose() {
173
+ clearDwell(); clearGrace();
174
+ anchor?.style.removeProperty('anchor-name');
175
+ },
176
+ };
177
+ });
178
+ if (!folded && !mounted) setMounted(true);
179
+ react.useEffect(() => { if (folded) hover.dismiss(); }, [folded, hover]);
180
+ react.useEffect(() => { hover.refresh(rows, t, now); }, [hover, rows, t, now]);
181
+ react.useEffect(() => () => hover.dispose(), [hover]);
182
+ const reduce = react.useRef(false);
183
+ react.useEffect(() => {
184
+ const query = matchMedia('(prefers-reduced-motion: reduce)');
185
+ const change = () => { reduce.current = query.matches; if (query.matches && folded) setMounted(false); };
186
+ change(); query.addEventListener('change', change);
187
+ return () => query.removeEventListener('change', change);
188
+ }, [folded]);
189
+ react.useEffect(() => {
190
+ let frame; let timer;
191
+ if (!folded) { setMounted(true); frame = requestAnimationFrame(() => setShown(true)); }
192
+ else { setShown(false); if (reduce.current) setMounted(false); else timer = setTimeout(() => setMounted(false), 190); }
193
+ return () => { cancelAnimationFrame(frame); clearTimeout(timer); };
194
+ }, [folded]);
195
+ return react.createElement(RecentHoverContext.Provider, { value: hover },
196
+ react.createElement('div', { className: 'desktop-navigation-recent-motion', 'data-open': shown && !folded ? '' : undefined, 'aria-hidden': folded ? 'true' : undefined, ref: element => { if (element) element.inert = folded; } },
197
+ react.createElement('div', { className: 'desktop-navigation-recent-clip' }, mounted ? react.createElement('ul', { className }, children) : null),
198
+ react.createElement('style', null, '.desktop-navigation-recent-motion{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows 180ms ease,opacity 140ms ease}.desktop-navigation-recent-motion[data-open]{grid-template-rows:1fr;opacity:1}.desktop-navigation-recent-clip{min-height:0;overflow:hidden}.desktop-navigation-recent-card{--dsw-hovercard-bg:#2C2C2E;position:fixed;position-anchor:--dsh-recent-hover-anchor;left:calc(anchor(right) + 8px);top:anchor(top);position-try-fallbacks:--dsh-recent-bottom;z-index:100;box-sizing:border-box;width:244px;padding:12px 16px;border-radius:var(--dsw-radius-lg);background:var(--dsw-hovercard-bg);box-shadow:var(--dsw-shadow-lv3);cursor:pointer}@position-try --dsh-recent-bottom{top:auto;bottom:8px}.desktop-navigation-recent-card:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}.desktop-navigation-recent-copied{color:#fff;font-size:14px;line-height:20px;text-align:center}.aalpzW_chevron{transition:transform 180ms ease!important}@media(prefers-reduced-motion:reduce){.desktop-navigation-recent-motion,.aalpzW_chevron,[data-dsh-recent-section-chevron]{transition:none!important}}')),
199
+ card && react_dom.createPortal(react.createElement('div', {
200
+ className: 'desktop-navigation-recent-card',
201
+ role: 'button', tabIndex: 0,
202
+ 'aria-label': card.t('copy') + ': ' + card.row.title,
203
+ onPointerEnter: hover.enterCard, onPointerLeave: hover.leaveCard,
204
+ onClick: () => {
205
+ const id = card.row.id;
206
+ if (!navigator.clipboard?.writeText) return;
207
+ navigator.clipboard.writeText(card.row.title).then(() => {
208
+ if (hover.currentId() !== id) return;
209
+ setCopied(true);
210
+ setTimeout(() => { if (hover.currentId() === id) setCopied(false); }, 1000);
211
+ }).catch(() => {});
212
+ },
213
+ onKeyDown: event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); event.currentTarget.click(); } },
214
+ }, copied ? react.createElement('span', { className: 'desktop-navigation-recent-copied', 'aria-hidden': true }, card.t('hover.copied')) : react.createElement(RecentRowCard, { row: card.row, t: card.t, now: card.now })), document.body));
215
+ }