@ibgib/space-gib 0.0.34 → 0.0.35

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 (45) hide show
  1. package/dist/client/bootstrap.mjs +1 -1
  2. package/dist/client/{chunk-25PXC4HP.mjs → chunk-FIAGRVBY.mjs} +1487 -468
  3. package/dist/client/chunk-XLQ3UCQY.mjs +53 -0
  4. package/dist/client/css/activity-bar.css +21 -3
  5. package/dist/client/css/layout.css +126 -5
  6. package/dist/client/index.html +6 -0
  7. package/dist/client/index.mjs +22 -45
  8. package/dist/client/privacy.html +22 -22
  9. package/dist/client/script.mjs +1 -1
  10. package/dist/server/server.mjs +3 -3
  11. package/package.json +6 -6
  12. package/src/client/AUTO-GENERATED-version.mts +1 -1
  13. package/src/client/cosmos/seed-cosmos-workspace.mts +20 -6
  14. package/src/client/css/activity-bar.css +21 -3
  15. package/src/client/css/layout.css +126 -5
  16. package/src/client/dev-tools/vcs-workspace.mts +4 -4
  17. package/src/client/index.html +6 -0
  18. package/src/client/privacy.html +22 -22
  19. package/src/client/ui/component/changes/changes.css +413 -0
  20. package/src/client/ui/component/changes/changes.html +37 -0
  21. package/src/client/ui/component/changes/changes.mts +766 -0
  22. package/src/client/ui/component/changes/index.mts +9 -0
  23. package/src/client/ui/component/chat-view/chat-view.html +6 -58
  24. package/src/client/ui/component/chat-view/chat-view.mts +13 -26
  25. package/src/client/ui/component/clone/clone.mts +67 -39
  26. package/src/client/ui/component/code-editor/code-editor.css +91 -9
  27. package/src/client/ui/component/code-editor/code-editor.html +58 -45
  28. package/src/client/ui/component/code-editor/code-editor.mts +265 -49
  29. package/src/client/ui/component/cosmos-navigator/cosmos-navigator.css +1 -1
  30. package/src/client/ui/component/cosmos-navigator/cosmos-navigator.html +5 -10
  31. package/src/client/ui/component/cosmos-navigator/cosmos-navigator.mts +297 -64
  32. package/src/client/ui/component/file-explorer/file-explorer.css +475 -11
  33. package/src/client/ui/component/file-explorer/file-explorer.html +45 -65
  34. package/src/client/ui/component/file-explorer/file-explorer.mts +1164 -98
  35. package/src/client/ui/component/nested-chat/chat/chat.mts +5 -90
  36. package/src/client/ui/component/nested-chat/nested-chat.mts +10 -8
  37. package/src/client/ui/component/timeline/timeline.css +16 -1
  38. package/src/client/ui/component/timeline/timeline.mts +37 -93
  39. package/src/client/ui/router/space-gib-router-helpers.mts +33 -233
  40. package/src/client/ui/router/space-gib-router-helpers.respec.mts +298 -122
  41. package/src/client/ui/router/space-gib-router-service.mts +28 -151
  42. package/src/client/ui/router/space-gib-router-types.mts +32 -76
  43. package/src/client/ui/shell/cosmic-big-bang.mts +12 -3
  44. package/src/client/ui/shell/space-gib-shell-service.mts +687 -119
  45. package/dist/client/chunk-RXWLL2AI.mjs +0 -30
@@ -166,106 +166,21 @@ export class ChatComponentInstance
166
166
  const { ib } = getIbAndGib({ ibGibAddr: addr });
167
167
 
168
168
  if (addr === ROOT_ADDR || ib === 'root' || ib.includes('space-gib')) {
169
- this.el.contextTitleEl.textContent = 'Thread: src/index.ts';
169
+ this.el.contextTitleEl.textContent = 'Thread: Workspace';
170
170
  } else if (ib.startsWith('comment')) {
171
171
  const pieces = ib.split(' ');
172
172
  const preview = pieces.length > 1 ? pieces.slice(1).join(' ') : 'Comment Thread';
173
173
  this.el.contextTitleEl.textContent = `Thread: "${preview.slice(0, 24)}..."`;
174
+ } else if (ib.startsWith('file ')) {
175
+ const file = ib.replace('file ', '');
176
+ this.el.contextTitleEl.textContent = `Thread: ${file}`;
174
177
  } else {
175
178
  this.el.contextTitleEl.textContent = `Thread: ${ib.slice(0, 20)}`;
176
179
  }
177
180
  }
178
181
 
179
182
  private loadInitialMessages(): void {
180
- const addr = this.ibGibAddr ?? ROOT_ADDR;
181
- const { ib } = getIbAndGib({ ibGibAddr: addr });
182
-
183
- if (ib.includes('boundary') || ib.includes('try/catch') || ib.includes('boot')) {
184
- // Nested replies for the error boundary comment thread
185
- this.messages = [
186
- {
187
- id: 'submsg-1',
188
- addr: 'comment boundary-rationale^gib_b1',
189
- author: 'Charlie',
190
- avatar: '👩‍🔬',
191
- role: 'Peer',
192
- timestamp: '3m ago',
193
- text: 'Wrapping boot() in a top-level error boundary prevents uncaught rejections from freezing the virtual canvas.',
194
- replyCount: 1,
195
- },
196
- {
197
- id: 'submsg-2',
198
- addr: 'comment boundary-event^gib_b2',
199
- author: 'Bob',
200
- avatar: '👨‍🚀',
201
- role: 'Peer',
202
- timestamp: '1m ago',
203
- text: 'Agreed! Let us also dispatch an EVENT_IBGIB_UI_MESSAGE so the status bar shows the error banner.',
204
- replyCount: 0,
205
- }
206
- ];
207
- } else if (ib.includes('rationale') || ib.includes('index.ts') || ib.includes('indexeddb')) {
208
- // Nested replies for the architectural rationale comment thread
209
- this.messages = [
210
- {
211
- id: 'submsg-3',
212
- addr: 'comment offline-merkle^gib_a1',
213
- author: 'Bob',
214
- avatar: '👨‍🚀',
215
- role: 'Peer',
216
- timestamp: '5m ago',
217
- text: 'The local IndexedDB space works completely offline without network roundtrips.',
218
- replyCount: 0,
219
- },
220
- {
221
- id: 'submsg-4',
222
- addr: 'comment sync-keystone^gib_a2',
223
- author: 'Alice',
224
- avatar: '👩‍💻',
225
- role: 'Keystone',
226
- timestamp: '2m ago',
227
- text: 'Exactly! Remote peer hubs synchronize automatically via WebSockets whenever connectivity is detected.',
228
- replyCount: 0,
229
- }
230
- ];
231
- } else {
232
- // Default file / root comments
233
- this.messages = [
234
- {
235
- id: 'msg-1',
236
- addr: 'comment index-arch-rationale^gib_1',
237
- author: 'Alice',
238
- avatar: '👩‍💻',
239
- role: 'Keystone',
240
- timestamp: '10m ago',
241
- anchor: 'src/index.ts#L1-18',
242
- text: 'Architectural Rationale: This entrypoint initializes the virtual repository workspace within the browser’s local IndexedDB metaspace.',
243
- replyCount: 2,
244
- },
245
- {
246
- id: 'msg-2',
247
- addr: 'comment index-try-catch^gib_2',
248
- author: 'Bob',
249
- avatar: '👨‍🚀',
250
- role: 'Peer',
251
- timestamp: '6m ago',
252
- anchor: 'src/index.ts#L36-42',
253
- text: 'Looks super clean! Should we add an async try/catch boundary around boot() as well?',
254
- replyCount: 2,
255
- },
256
- {
257
- id: 'msg-3',
258
- addr: 'comment index-agent-analysis^gib_3',
259
- author: 'Agent',
260
- avatar: '🤖',
261
- role: 'Agent',
262
- timestamp: '4m ago',
263
- anchor: 'src/index.ts#L43-50',
264
- text: 'Static analysis confirms that initOrchestration provides a root failure boundary.',
265
- replyCount: 0,
266
- }
267
- ];
268
- }
183
+ this.messages = [];
269
184
  }
270
185
 
271
186
  private async initEnterBehavior(): Promise<void> {
@@ -99,7 +99,7 @@ export class NestedChatComponentInstance
99
99
 
100
100
  private ensureValidIbGibAddr(addr: string): IbGibAddr {
101
101
  if (!addr || addr === ROOT_ADDR) {
102
- return 'file src/index.ts^index_tjp';
102
+ return ROOT_ADDR;
103
103
  }
104
104
  if (!addr.includes('^')) {
105
105
  return `file ${addr}^${addr.replace(/[^a-zA-Z0-9]/g, '_')}_tjp`;
@@ -123,13 +123,15 @@ export class NestedChatComponentInstance
123
123
 
124
124
  this.bindEvents();
125
125
 
126
- // Only activate initial thread if an ibGibAddr was explicitly provided to the component
127
- if (this.ibGibAddr) {
126
+ // Only activate initial thread if an ibGibAddr was explicitly provided (not ROOT_ADDR)
127
+ if (this.ibGibAddr && this.ibGibAddr !== ROOT_ADDR) {
128
128
  const rootAddr = this.ensureValidIbGibAddr(this.ibGibAddr);
129
- setTimeout(async () => {
130
- console.log(`${lc} activating initial root thread: ${rootAddr}`);
131
- await this.activateIbGib({ addr: rootAddr });
132
- }, 0);
129
+ if (rootAddr && rootAddr !== ROOT_ADDR) {
130
+ setTimeout(async () => {
131
+ console.log(`${lc} activating initial root thread: ${rootAddr}`);
132
+ await this.activateIbGib({ addr: rootAddr });
133
+ }, 0);
134
+ }
133
135
  }
134
136
 
135
137
  } catch (error) {
@@ -350,7 +352,7 @@ export class NestedChatComponentInstance
350
352
 
351
353
  const { ib } = getIbAndGib({ ibGibAddr: addr });
352
354
  if (addr === ROOT_ADDR || ib === 'root' || ib.includes('file') || !addr.includes('comment')) {
353
- const fileTitle = ib.startsWith('file ') ? ib.replace('file ', '') : 'src/index.ts';
355
+ const fileTitle = ib.startsWith('file ') ? ib.replace('file ', '') : (ib === 'root' || addr === ROOT_ADDR ? 'Workspace' : ib);
354
356
  return {
355
357
  shortTitle: this.formatShortTitle(fileTitle),
356
358
  fullText: `Root Thread: ${fileTitle}`,
@@ -2,15 +2,25 @@
2
2
  display: flex;
3
3
  flex-direction: column;
4
4
  width: 100%;
5
+ height: 100%;
6
+ min-height: 0;
5
7
  box-sizing: border-box;
6
8
  margin: 0;
7
9
  padding: 0;
8
10
  }
9
11
 
12
+ :host([collapsed]) {
13
+ height: auto !important;
14
+ flex: 0 0 auto !important;
15
+ }
16
+
10
17
  .timeline-root {
11
18
  display: flex;
12
19
  flex-direction: column;
13
20
  width: 100%;
21
+ height: 100%;
22
+ min-height: 0;
23
+ overflow: hidden;
14
24
  user-select: none;
15
25
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
16
26
  box-sizing: border-box;
@@ -23,7 +33,7 @@
23
33
  display: flex;
24
34
  align-items: center;
25
35
  gap: 0.35rem;
26
- padding: 0.4rem 0.6rem;
36
+ padding: 0.28rem 0.5rem;
27
37
  background: rgba(255, 255, 255, 0.04);
28
38
  border: 1px solid rgba(255, 255, 255, 0.08);
29
39
  border-radius: 6px;
@@ -35,6 +45,8 @@
35
45
  outline: none;
36
46
  box-sizing: border-box;
37
47
  margin: 0;
48
+ flex: 0 0 auto !important;
49
+ flex-shrink: 0 !important;
38
50
  }
39
51
 
40
52
  .timeline-header:hover {
@@ -135,6 +147,9 @@
135
147
  .timeline-stream-container {
136
148
  display: flex;
137
149
  flex-direction: column;
150
+ flex: 1 1 0px;
151
+ min-height: 0;
152
+ overflow-y: auto;
138
153
  position: relative;
139
154
  padding: 0.35rem 0.2rem 0.5rem 0.8rem;
140
155
  box-sizing: border-box;
@@ -87,98 +87,8 @@ export class TimelineComponentInstance
87
87
 
88
88
  private branchName: string = 'main';
89
89
  private isTimelineExpanded: boolean = true;
90
- private selectedHash: string = '3e8f1c2';
91
-
92
- private snapshots: TimelineItem[] = [
93
- {
94
- hash: '3e8f1c2a05d8f99e32b4f17c802e3b4a',
95
- shortHash: '3e8f1c2',
96
- message: 'feat: implement cosmos navigation and orbital drift',
97
- author: 'Bill Raiford <bill@ibgib.space>',
98
- timestamp: '2m ago',
99
- isTip: true,
100
- branch: 'main',
101
- parentHash: 'a7b4d90',
102
- changedFiles: [
103
- { path: 'src/client/ui/component/cosmos-navigator/cosmos-navigator.mts', status: 'M', additions: 18, deletions: 4 },
104
- { path: 'src/client/ui/shell/space-gib-shell-service.mts', status: 'M', additions: 32, deletions: 6 },
105
- { path: 'src/client/css/layout.css', status: 'M', additions: 14, deletions: 2 },
106
- ],
107
- diffPreview: `@@ -848,6 +848,20 @@
108
- - this.launchProjectile({ ... });
109
- + // Make activity bar visible BEFORE zoom flight starts
110
- + window.dispatchEvent(new CustomEvent('space-gib-activity-bar-expand-requested'));
111
- + setTimeout(() => {
112
- + this.launchProjectile({ ... });
113
- + }, 180);`
114
- },
115
- {
116
- hash: 'a7b4d9089f21c4320b9e8471da329188',
117
- shortHash: 'a7b4d90',
118
- message: 'style: polish firecracker big bang compression',
119
- author: 'Bill Raiford <bill@ibgib.space>',
120
- timestamp: '1h ago',
121
- branch: 'main',
122
- parentHash: 'f2098b1',
123
- changedFiles: [
124
- { path: 'src/client/ui/component/cosmos-navigator/cosmos-navigator.css', status: 'M', additions: 24, deletions: 11 },
125
- { path: 'src/client/ui/component/cosmos-navigator/cosmos-navigator.mts', status: 'M', additions: 12, deletions: 5 },
126
- ],
127
- diffPreview: `@@ -430,8 +430,12 @@
128
- - btn.classList.add('compressing');
129
- + // Firecracker high velocity rotation
130
- + btn.classList.add('compressing');
131
- + this.triggerBigBangExplosion();`
132
- },
133
- {
134
- hash: 'f2098b191c4998721fae390c91834789',
135
- shortHash: 'f2098b1',
136
- message: 'refactor: decouple activity bar and cosmos controls',
137
- author: 'Bill Raiford <bill@ibgib.space>',
138
- timestamp: 'yesterday',
139
- branch: 'main',
140
- parentHash: 'c019a4e',
141
- changedFiles: [
142
- { path: 'src/client/ui/shell/cosmic-big-bang.mts', status: 'M', additions: 40, deletions: 52 },
143
- { path: 'src/client/ui/shell/space-gib-shell-service.mts', status: 'M', additions: 75, deletions: 18 },
144
- ],
145
- diffPreview: `@@ -120,7 +120,7 @@
146
- - public toggleActivityBar(): void {
147
- + public expandActivityBar(): void {`
148
- },
149
- {
150
- hash: 'c019a4ee899201bdcf88132582910fa2',
151
- shortHash: 'c019a4e',
152
- message: 'feat: primordial singularity and big bang stage',
153
- author: 'Bill Raiford <bill@ibgib.space>',
154
- timestamp: '3d ago',
155
- branch: 'main',
156
- parentHash: '0000000',
157
- changedFiles: [
158
- { path: 'src/client/ui/component/cosmos-navigator/cosmos-navigator.mts', status: 'A', additions: 450, deletions: 0 },
159
- { path: 'src/client/ui/component/cosmos-navigator/cosmos-navigator.html', status: 'A', additions: 95, deletions: 0 },
160
- ],
161
- diffPreview: `@@ -0,0 +1,95 @@
162
- +<div id="cosmos-nav-root" class="cosmos-nav-root">
163
- + <canvas id="bg-canvas"></canvas>
164
- +</div>`
165
- },
166
- {
167
- hash: '00000000000000000000000000000000',
168
- shortHash: '0000000',
169
- message: 'Initial commit (empty space genesis)',
170
- author: 'Bill Raiford <bill@ibgib.space>',
171
- timestamp: '5d ago',
172
- branch: 'main',
173
- changedFiles: [
174
- { path: 'package.json', status: 'A', additions: 35, deletions: 0 },
175
- { path: 'README.md', status: 'A', additions: 20, deletions: 0 },
176
- ],
177
- diffPreview: `@@ -0,0 +1,20 @@
178
- +# space-gib
179
- +Decentralized VCS and superspaces.`
180
- }
181
- ];
90
+ private selectedHash: string = '';
91
+ private snapshots: TimelineItem[] = [];
182
92
 
183
93
  get el(): TimelineElements {
184
94
  if (!this.elements) {
@@ -203,6 +113,7 @@ export class TimelineComponentInstance
203
113
  contentEl: shadow.getElementById('timeline-root') as HTMLDivElement,
204
114
  };
205
115
 
116
+ this.toggleAttribute('collapsed', !this.isTimelineExpanded);
206
117
  this.bindHandlers();
207
118
  this.renderSnapshots();
208
119
  } catch (error) {
@@ -238,6 +149,15 @@ export class TimelineComponentInstance
238
149
  if (!container) { return; }
239
150
  container.innerHTML = '';
240
151
 
152
+ if (this.snapshots.length === 0) {
153
+ container.innerHTML = `
154
+ <div class="empty-timeline-msg" style="padding: 16px 12px; font-size: 11px; color: #8a99a8; font-style: italic; text-align: center;">
155
+ No commits on branch [${this.branchName}] yet.
156
+ </div>
157
+ `;
158
+ return;
159
+ }
160
+
241
161
  for (const item of this.snapshots) {
242
162
  const row = document.createElement('div');
243
163
  row.className = `timeline-item ${item.shortHash === this.selectedHash ? 'selected' : ''} ${item.isTip ? 'is-tip' : ''}`;
@@ -268,6 +188,11 @@ export class TimelineComponentInstance
268
188
  }
269
189
  }
270
190
 
191
+ public setSnapshots(snapshots: TimelineItem[]): void {
192
+ this.snapshots = snapshots || [];
193
+ this.renderSnapshots();
194
+ }
195
+
271
196
  public selectTimelineItem(hash: string): void {
272
197
  this.selectedHash = hash;
273
198
  const shadow = this.shadowRoot;
@@ -288,8 +213,23 @@ export class TimelineComponentInstance
288
213
  }
289
214
  }
290
215
 
216
+ public addSnapshot(snapshot: TimelineItem): void {
217
+ for (const s of this.snapshots) {
218
+ s.isTip = false;
219
+ }
220
+ snapshot.isTip = true;
221
+ this.snapshots.unshift(snapshot);
222
+ this.renderSnapshots();
223
+ }
224
+
225
+ public clearSnapshots(): void {
226
+ this.snapshots = [];
227
+ this.renderSnapshots();
228
+ }
229
+
291
230
  public toggleTimelineAccordion(): void {
292
231
  this.isTimelineExpanded = !this.isTimelineExpanded;
232
+ this.toggleAttribute('collapsed', !this.isTimelineExpanded);
293
233
  const chevron = this.el.timelineHeaderEl.querySelector('.chevron-icon');
294
234
 
295
235
  if (this.isTimelineExpanded) {
@@ -301,9 +241,13 @@ export class TimelineComponentInstance
301
241
  this.el.timelineHeaderEl.classList.remove('expanded');
302
242
  this.el.timelineHeaderEl.classList.add('collapsed');
303
243
  this.el.timelineStreamContainerEl.classList.add('hidden');
304
- // if (chevron) chevron.textContent = '▶';
305
244
  if (chevron) { chevron.textContent = '▼'; }
306
245
  }
246
+ this.dispatchEvent(new CustomEvent('space-gib-accordion-toggled', {
247
+ detail: { isExpanded: this.isTimelineExpanded },
248
+ bubbles: true,
249
+ composed: true,
250
+ }));
307
251
  }
308
252
 
309
253
  override async disconnected(): Promise<void> {
@@ -2,199 +2,50 @@
2
2
  * @module client/ui/router/space-gib-router-helpers
3
3
  *
4
4
  * Route parsing and formatting helpers for the Space-Gib Client Router.
5
- * Pure isomorphic functions with zero DOM dependency.
5
+ * Delegates universal Cosmos & Galaxy routes to @ibgib/web-gib,
6
+ * while handling space-gib specific routes (/admin/review).
6
7
  */
7
8
 
8
9
  import {
9
- SpaceGibRouteInfo, SpaceGibTenant,
10
+ parseCosmosRoute,
11
+ formatCosmosRoute,
12
+ isSameCosmosRoute,
13
+ safeDecode,
14
+ ParseCosmosRouteOptions,
15
+ CosmosRouteInfo,
16
+ } from '@ibgib/web-gib/dist/ui/router/index.mjs';
17
+
18
+ import {
19
+ SpaceGibRouteInfo,
20
+ AdminReviewRouteInfo,
10
21
  } from './space-gib-router-types.mjs';
11
22
 
12
- /**
13
- * Safely decodes a URI component, returning the raw string if decoding fails.
14
- */
15
- function safeDecode(str: string): string {
16
- try {
17
- return decodeURIComponent(str);
18
- } catch {
19
- return str;
20
- }
21
- }
23
+ export { safeDecode };
24
+ export type ParseSpaceGibRouteOptions = ParseCosmosRouteOptions;
22
25
 
23
26
  /**
24
27
  * Parses a browser pathname into a structured SpaceGibRouteInfo.
25
- *
26
- * Supports:
27
- * - Polycosmos: / or /polycosmos or /@:siteUsername or /keystone/:keystoneTjp
28
- * - Cosmos: /:cosmos or /@:siteUsername/:cosmos or /keystone/:keystoneTjp/:cosmos
29
- * - Code Galaxy: /:cosmos/code/:galaxyId[/:branch[/*filePath]]
30
- * - Notes Galaxy: /:cosmos/notes/:galaxyId[/:topicTjp]
31
- * - Raw Addr (Snapshot): /:cosmos/:spaceId/addr/:ibGibAddr
32
- * - Raw Tjp (Living Tip): /:cosmos/:spaceId/tjp/:tjpAddr
33
- * - Admin Review: /admin/review[/:tjpGib]
28
+ * Handles /admin/review explicitly, and delegates all Cosmos / Galaxy / Raw routes to @ibgib/web-gib.
34
29
  */
35
- export function parseSpaceGibRoute(pathname: string): SpaceGibRouteInfo {
30
+ export function parseSpaceGibRoute(pathname: string, opts?: ParseSpaceGibRouteOptions): SpaceGibRouteInfo {
36
31
  let p = (pathname || '').trim();
37
32
 
38
- let queryObj: Record<string, string> | undefined;
39
- const qIdx = p.indexOf('?');
40
- if (qIdx !== -1) {
41
- const qStr = p.substring(qIdx + 1);
42
- p = p.substring(0, qIdx);
43
- const searchParams = new URLSearchParams(qStr);
44
- const q: Record<string, string> = {};
45
- searchParams.forEach((val, key) => {
46
- q[key] = val;
47
- });
48
- if (Object.keys(q).length > 0) {
49
- queryObj = q;
50
- }
51
- }
52
- const hIdx = p.indexOf('#');
53
- if (hIdx !== -1) { p = p.substring(0, hIdx); }
33
+ // Check query string/hash for /admin/review prefix
34
+ let cleanPath = p;
35
+ const qIdx = cleanPath.indexOf('?');
36
+ if (qIdx !== -1) { cleanPath = cleanPath.substring(0, qIdx); }
37
+ const hIdx = cleanPath.indexOf('#');
38
+ if (hIdx !== -1) { cleanPath = cleanPath.substring(0, hIdx); }
54
39
 
55
- if (!p || p === '/') {
56
- return { kind: 'polycosmos' };
57
- }
58
-
59
- // Special Route: /admin/review
60
- if (p === '/admin/review') {
40
+ if (cleanPath === '/admin/review') {
61
41
  return { kind: 'admin-review' };
62
42
  }
63
- if (p.startsWith('/admin/review/')) {
64
- const tjpGib = safeDecode(p.slice('/admin/review/'.length)).trim();
43
+ if (cleanPath.startsWith('/admin/review/')) {
44
+ const tjpGib = safeDecode(cleanPath.slice('/admin/review/'.length)).trim();
65
45
  return { kind: 'admin-review', ...(tjpGib ? { tjpGib } : {}) };
66
46
  }
67
47
 
68
- const rawSegments = p.split('/').filter(s => s.length > 0);
69
- if (rawSegments.length === 0) {
70
- return { kind: 'polycosmos' };
71
- }
72
-
73
- // Extract tenant prefix if present (@username or /keystone/tjp)
74
- let tenant: SpaceGibTenant | undefined;
75
- let segIdx = 0;
76
-
77
- if (rawSegments[0].startsWith('@')) {
78
- const username = safeDecode(rawSegments[0].slice(1)).trim();
79
- if (username) {
80
- tenant = { type: 'username', siteUsername: username };
81
- segIdx = 1;
82
- }
83
- } else if (rawSegments[0] === 'keystone' && rawSegments.length > 1) {
84
- const keystoneTjp = safeDecode(rawSegments[1]).trim();
85
- if (keystoneTjp) {
86
- tenant = { type: 'keystone', keystoneTjp };
87
- segIdx = 2;
88
- }
89
- }
90
-
91
- const remaining = rawSegments.slice(segIdx);
92
-
93
- // If only tenant specifier was given (e.g. /@bill or /keystone/tjp123)
94
- if (remaining.length === 0) {
95
- return { kind: 'polycosmos', ...(tenant ? { tenant } : {}) };
96
- }
97
-
98
- if (remaining.length === 1 && remaining[0] === 'polycosmos') {
99
- return { kind: 'polycosmos', ...(tenant ? { tenant } : {}) };
100
- }
101
-
102
- const cosmos = safeDecode(remaining[0]).trim();
103
-
104
- if (remaining.length === 1) {
105
- return { kind: 'cosmos', ...(tenant ? { tenant } : {}), cosmos };
106
- }
107
-
108
- const second = safeDecode(remaining[1]).trim();
109
-
110
- // Code Galaxy: /:cosmos/code/:galaxyId[/:branch[/*filePath]]
111
- if (second === 'code') {
112
- const galaxyId = remaining[2] ? safeDecode(remaining[2]).trim() : '';
113
- if (!galaxyId) {
114
- return { kind: 'cosmos', ...(tenant ? { tenant } : {}), cosmos };
115
- }
116
- const branch = remaining[3] ? safeDecode(remaining[3]).trim() : undefined;
117
- const filePath = remaining.length > 4 ? remaining.slice(4).map(safeDecode).join('/') : undefined;
118
- return {
119
- kind: 'code-galaxy',
120
- ...(tenant ? { tenant } : {}),
121
- cosmos,
122
- galaxyId,
123
- ...(branch ? { branch } : {}),
124
- ...(filePath ? { filePath } : {}),
125
- };
126
- }
127
-
128
- // Notes Galaxy: /:cosmos/notes/:galaxyId[/tjp/:tjp1/:tjp2...] or [/addr/:addr1/:addr2...] or [/:topicTjp]
129
- if (second === 'notes') {
130
- const galaxyId = remaining[2] ? safeDecode(remaining[2]).trim() : '';
131
- if (!galaxyId) {
132
- return { kind: 'cosmos', ...(tenant ? { tenant } : {}), cosmos };
133
- }
134
-
135
- let trailMode: 'tjp' | 'addr' | undefined;
136
- let trail: string[] | undefined;
137
- let topicTjp: string | undefined;
138
-
139
- if (remaining.length > 3) {
140
- const modeCandidate = safeDecode(remaining[3]).trim();
141
- if (modeCandidate === 'tjp' || modeCandidate === 'addr') {
142
- trailMode = modeCandidate;
143
- const trailSegments = remaining.slice(4).map(s => safeDecode(s).trim()).filter(s => s.length > 0);
144
- if (trailSegments.length > 0) {
145
- trail = trailSegments;
146
- topicTjp = trailSegments[0];
147
- }
148
- } else {
149
- // Backward compatibility / shorthand: /notes/:galaxyId/:topicTjp
150
- topicTjp = modeCandidate;
151
- trailMode = 'tjp';
152
- trail = [modeCandidate];
153
- }
154
- }
155
-
156
- return {
157
- kind: 'notes-galaxy',
158
- ...(tenant ? { tenant } : {}),
159
- cosmos,
160
- galaxyId,
161
- ...(trailMode ? { trailMode } : {}),
162
- ...(trail ? { trail } : {}),
163
- ...(topicTjp ? { topicTjp } : {}),
164
- ...(queryObj ? { query: queryObj } : {}),
165
- };
166
- }
167
-
168
- // Mechanism 1: Space-Qualified Raw Addrs
169
- // /:cosmos/:spaceId/addr/:ibGibAddr
170
- // /:cosmos/:spaceId/tjp/:tjpAddr
171
- if (remaining.length >= 4) {
172
- const third = safeDecode(remaining[2]).trim();
173
- const spaceId = second;
174
- if (third === 'addr') {
175
- const ibGibAddr = safeDecode(remaining.slice(3).join('/')).trim();
176
- return {
177
- kind: 'raw-addr',
178
- ...(tenant ? { tenant } : {}),
179
- cosmos,
180
- spaceId,
181
- ibGibAddr,
182
- };
183
- }
184
- if (third === 'tjp') {
185
- const tjpAddr = safeDecode(remaining.slice(3).join('/')).trim();
186
- return {
187
- kind: 'raw-tjp',
188
- ...(tenant ? { tenant } : {}),
189
- cosmos,
190
- spaceId,
191
- tjpAddr,
192
- };
193
- }
194
- }
195
-
196
- // Fallback: Cosmos route
197
- return { kind: 'cosmos', ...(tenant ? { tenant } : {}), cosmos };
48
+ return parseCosmosRoute(pathname, opts) as SpaceGibRouteInfo;
198
49
  }
199
50
 
200
51
  /**
@@ -204,62 +55,7 @@ export function formatSpaceGibRoute(route: SpaceGibRouteInfo): string {
204
55
  if (route.kind === 'admin-review') {
205
56
  return `/admin/review${route.tjpGib ? `/${encodeURIComponent(route.tjpGib)}` : ''}`;
206
57
  }
207
-
208
- let prefix = '';
209
- if (route.tenant) {
210
- if (route.tenant.type === 'username') {
211
- prefix = `/@${encodeURIComponent(route.tenant.siteUsername)}`;
212
- } else if (route.tenant.type === 'keystone') {
213
- prefix = `/keystone/${encodeURIComponent(route.tenant.keystoneTjp)}`;
214
- }
215
- }
216
-
217
- switch (route.kind) {
218
- case 'polycosmos':
219
- return prefix ? `${prefix}/polycosmos` : '/';
220
-
221
- case 'cosmos':
222
- return `${prefix}/${encodeURIComponent(route.cosmos)}`;
223
-
224
- case 'code-galaxy': {
225
- let res = `${prefix}/${encodeURIComponent(route.cosmos)}/code/${encodeURIComponent(route.galaxyId)}`;
226
- if (route.branch) {
227
- res += `/${encodeURIComponent(route.branch)}`;
228
- if (route.filePath) {
229
- res += `/${route.filePath.split('/').map(encodeURIComponent).join('/')}`;
230
- }
231
- }
232
- return res;
233
- }
234
-
235
- case 'notes-galaxy': {
236
- let res = `${prefix}/${encodeURIComponent(route.cosmos)}/notes/${encodeURIComponent(route.galaxyId)}`;
237
- if (route.trail && route.trail.length > 0) {
238
- const mode = route.trailMode || 'tjp';
239
- res += `/${mode}/${route.trail.map(encodeURIComponent).join('/')}`;
240
- } else if (route.topicTjp) {
241
- res += `/${encodeURIComponent(route.topicTjp)}`;
242
- }
243
- if (route.query && Object.keys(route.query).length > 0) {
244
- const sp = new URLSearchParams();
245
- for (const [k, v] of Object.entries(route.query)) {
246
- if (v !== undefined) { sp.set(k, v); }
247
- }
248
- const qs = sp.toString();
249
- if (qs) { res += `?${qs}`; }
250
- }
251
- return res;
252
- }
253
-
254
- case 'raw-addr':
255
- return `${prefix}/${encodeURIComponent(route.cosmos)}/${encodeURIComponent(route.spaceId)}/addr/${encodeURIComponent(route.ibGibAddr)}`;
256
-
257
- case 'raw-tjp':
258
- return `${prefix}/${encodeURIComponent(route.cosmos)}/${encodeURIComponent(route.spaceId)}/tjp/${encodeURIComponent(route.tjpAddr)}`;
259
-
260
- default:
261
- return '/';
262
- }
58
+ return formatCosmosRoute(route as CosmosRouteInfo);
263
59
  }
264
60
 
265
61
  /**
@@ -268,5 +64,9 @@ export function formatSpaceGibRoute(route: SpaceGibRouteInfo): string {
268
64
  export function isSameRoute(a?: SpaceGibRouteInfo, b?: SpaceGibRouteInfo): boolean {
269
65
  if (!a && !b) { return true; }
270
66
  if (!a || !b) { return false; }
271
- return formatSpaceGibRoute(a) === formatSpaceGibRoute(b);
67
+ if (a.kind === 'admin-review' || b.kind === 'admin-review') {
68
+ if (a.kind !== b.kind) { return false; }
69
+ return (a as AdminReviewRouteInfo).tjpGib === (b as AdminReviewRouteInfo).tjpGib;
70
+ }
71
+ return isSameCosmosRoute(a as CosmosRouteInfo, b as CosmosRouteInfo);
272
72
  }