@slycode/slycode 0.2.22 → 0.2.23

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 (147) hide show
  1. package/dist/bridge/api.d.ts +2 -1
  2. package/dist/bridge/api.js +114 -1
  3. package/dist/bridge/api.js.map +1 -1
  4. package/dist/bridge/git-utils.d.ts +9 -0
  5. package/dist/bridge/git-utils.js +49 -0
  6. package/dist/bridge/git-utils.js.map +1 -0
  7. package/dist/bridge/index.js +8 -2
  8. package/dist/bridge/index.js.map +1 -1
  9. package/dist/bridge/response-store.d.ts +46 -0
  10. package/dist/bridge/response-store.js +95 -0
  11. package/dist/bridge/response-store.js.map +1 -0
  12. package/dist/bridge/session-manager.d.ts +33 -1
  13. package/dist/bridge/session-manager.js +185 -2
  14. package/dist/bridge/session-manager.js.map +1 -1
  15. package/dist/bridge/types.d.ts +37 -0
  16. package/dist/data/scaffold-templates/tutorial-project/documentation/kanban.json +1 -1
  17. package/dist/messaging/bridge-client.d.ts +4 -0
  18. package/dist/messaging/bridge-client.js +20 -1
  19. package/dist/messaging/bridge-client.js.map +1 -1
  20. package/dist/messaging/index.js +38 -10
  21. package/dist/messaging/index.js.map +1 -1
  22. package/dist/scripts/kanban.js +448 -2
  23. package/dist/store/actions/checkpoint.md +1 -1
  24. package/dist/store/actions/context.md +1 -1
  25. package/dist/store/actions/create-card.md +1 -1
  26. package/dist/store/actions/deep-design.md +13 -3
  27. package/dist/store/actions/design-requirements.md +11 -1
  28. package/dist/store/actions/explore.md +1 -1
  29. package/dist/store/actions/onboard.md +2 -4
  30. package/dist/store/actions/summarize.md +1 -1
  31. package/dist/store/skills/kanban/SKILL.md +77 -3
  32. package/dist/web/.next/BUILD_ID +1 -1
  33. package/dist/web/.next/app-path-routes-manifest.json +1 -0
  34. package/dist/web/.next/build-manifest.json +2 -2
  35. package/dist/web/.next/prerender-manifest.json +3 -3
  36. package/dist/web/.next/routes-manifest.json +6 -0
  37. package/dist/web/.next/server/app/_global-error.html +2 -2
  38. package/dist/web/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  41. package/dist/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  42. package/dist/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  43. package/dist/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  44. package/dist/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  45. package/dist/web/.next/server/app/_not-found.html +1 -1
  46. package/dist/web/.next/server/app/_not-found.rsc +10 -10
  47. package/dist/web/.next/server/app/_not-found.segments/_full.segment.rsc +10 -10
  48. package/dist/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  49. package/dist/web/.next/server/app/_not-found.segments/_index.segment.rsc +5 -5
  50. package/dist/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  51. package/dist/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  52. package/dist/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  53. package/dist/web/.next/server/app/api/changelog/route/app-paths-manifest.json +3 -0
  54. package/dist/web/.next/server/app/api/changelog/route/build-manifest.json +11 -0
  55. package/dist/web/.next/server/app/api/changelog/route/server-reference-manifest.json +4 -0
  56. package/dist/web/.next/server/app/api/changelog/route.js +7 -0
  57. package/dist/web/.next/server/app/api/changelog/route.js.map +5 -0
  58. package/dist/web/.next/server/app/api/changelog/route.js.nft.json +1 -0
  59. package/dist/web/.next/server/app/api/changelog/route_client-reference-manifest.js +2 -0
  60. package/dist/web/.next/server/app/api/cli-assets/assistant/route.js.nft.json +1 -1
  61. package/dist/web/.next/server/app/api/cli-assets/fix/route.js.nft.json +1 -1
  62. package/dist/web/.next/server/app/api/cli-assets/import/route.js.nft.json +1 -1
  63. package/dist/web/.next/server/app/api/cli-assets/route.js.nft.json +1 -1
  64. package/dist/web/.next/server/app/api/cli-assets/store/preview/route.js.nft.json +1 -1
  65. package/dist/web/.next/server/app/api/cli-assets/store/route.js.nft.json +1 -1
  66. package/dist/web/.next/server/app/api/cli-assets/sync/route.js.nft.json +1 -1
  67. package/dist/web/.next/server/app/api/cli-assets/updates/route.js.nft.json +1 -1
  68. package/dist/web/.next/server/app/api/dashboard/route.js.nft.json +1 -1
  69. package/dist/web/.next/server/app/api/file/route.js.nft.json +1 -1
  70. package/dist/web/.next/server/app/api/git-status/route.js.nft.json +1 -1
  71. package/dist/web/.next/server/app/api/kanban/route.js.nft.json +1 -1
  72. package/dist/web/.next/server/app/api/kanban/stream/route.js.nft.json +1 -1
  73. package/dist/web/.next/server/app/api/projects/[id]/route.js.nft.json +1 -1
  74. package/dist/web/.next/server/app/api/projects/reorder/route.js.nft.json +1 -1
  75. package/dist/web/.next/server/app/api/projects/route.js.nft.json +1 -1
  76. package/dist/web/.next/server/app/api/scheduler/route.js.nft.json +1 -1
  77. package/dist/web/.next/server/app/api/search/route.js.nft.json +1 -1
  78. package/dist/web/.next/server/app/api/sly-actions/invalidate/route.js.nft.json +1 -1
  79. package/dist/web/.next/server/app/api/sly-actions/route.js.nft.json +1 -1
  80. package/dist/web/.next/server/app/api/version-check/route.js.nft.json +1 -1
  81. package/dist/web/.next/server/app/page.js.nft.json +1 -1
  82. package/dist/web/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/dist/web/.next/server/app/project/[id]/page.js.nft.json +1 -1
  84. package/dist/web/.next/server/app/project/[id]/page_client-reference-manifest.js +1 -1
  85. package/dist/web/.next/server/app-paths-manifest.json +1 -0
  86. package/dist/web/.next/server/chunks/[root-of-the-server]__a259539f._.js +3 -0
  87. package/dist/web/.next/server/chunks/_next-internal_server_app_api_changelog_route_actions_d6e239bf.js +3 -0
  88. package/dist/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_18324462.js +1 -1
  89. package/dist/web/.next/server/chunks/src_lib_scheduler_ts_03988e3e._.js +1 -1
  90. package/dist/web/.next/server/chunks/src_lib_scheduler_ts_7120457c._.js +1 -1
  91. package/dist/web/.next/server/chunks/ssr/[root-of-the-server]__1f5fc489._.js +4 -3
  92. package/dist/web/.next/server/chunks/ssr/{[root-of-the-server]__077f472c._.js → [root-of-the-server]__6183d28c._.js} +1 -1
  93. package/dist/web/.next/server/chunks/ssr/[root-of-the-server]__bcbe4bf2._.js +4 -3
  94. package/dist/web/.next/server/chunks/ssr/src_components_Dashboard_tsx_efc4dc27._.js +1 -1
  95. package/dist/web/.next/server/chunks/ssr/src_components_c4135402._.js +1 -1
  96. package/dist/web/.next/server/chunks/ssr/src_contexts_VoiceContext_tsx_cfba7292._.js +1 -1
  97. package/dist/web/.next/server/pages/404.html +1 -1
  98. package/dist/web/.next/server/pages/500.html +2 -2
  99. package/dist/web/.next/server/server-reference-manifest.js +1 -1
  100. package/dist/web/.next/server/server-reference-manifest.json +1 -1
  101. package/dist/web/.next/static/chunks/293449b828207656.css +1 -0
  102. package/dist/web/.next/static/chunks/{f55f3c8c1a52f80c.js → 3859477038c381ad.js} +1 -1
  103. package/dist/web/.next/static/chunks/{8415039c5941cf5c.js → 3a5721af09d1c753.js} +4 -3
  104. package/dist/web/.next/static/chunks/{8fb2a99c64580de7.js → 98311243e9a5a0ec.js} +1 -1
  105. package/dist/web/.next/static/chunks/{b8e0c1aeea4a14bc.js → a47f36b030917d1f.js} +1 -1
  106. package/dist/web/.next/static/chunks/d60c422421920130.js +5 -0
  107. package/dist/web/.next/static/chunks/{4049cceee6a49323.js → fa78afe3ceed998b.js} +1 -1
  108. package/dist/web/src/app/api/changelog/route.ts +45 -0
  109. package/dist/web/src/app/api/kanban/route.ts +52 -12
  110. package/dist/web/src/components/ActivityFeed.tsx +3 -0
  111. package/dist/web/src/components/BranchTab.tsx +115 -0
  112. package/dist/web/src/components/ChangelogModal.tsx +234 -0
  113. package/dist/web/src/components/Dashboard.tsx +11 -0
  114. package/dist/web/src/components/GlobalClaudePanel.tsx +6 -0
  115. package/dist/web/src/components/ProjectKanban.tsx +38 -8
  116. package/dist/web/src/components/VersionUpdateToast.tsx +6 -4
  117. package/dist/web/src/components/VoiceControlBar.tsx +1 -1
  118. package/dist/web/src/lib/scheduler.ts +2 -1
  119. package/dist/web/src/lib/types.ts +24 -0
  120. package/dist/web/tsconfig.tsbuildinfo +1 -1
  121. package/package.json +1 -1
  122. package/templates/changelog.json +242 -0
  123. package/templates/kanban-seed.json +1 -1
  124. package/templates/store/actions/checkpoint.md +1 -1
  125. package/templates/store/actions/context.md +1 -1
  126. package/templates/store/actions/create-card.md +1 -1
  127. package/templates/store/actions/deep-design.md +13 -3
  128. package/templates/store/actions/design-requirements.md +11 -1
  129. package/templates/store/actions/explore.md +1 -1
  130. package/templates/store/actions/onboard.md +2 -4
  131. package/templates/store/actions/summarize.md +1 -1
  132. package/templates/store/skills/kanban/SKILL.md +77 -3
  133. package/templates/tutorial-project/documentation/kanban.json +1 -1
  134. package/templates/updates/actions/checkpoint.md +1 -1
  135. package/templates/updates/actions/context.md +1 -1
  136. package/templates/updates/actions/create-card.md +1 -1
  137. package/templates/updates/actions/deep-design.md +13 -3
  138. package/templates/updates/actions/design-requirements.md +11 -1
  139. package/templates/updates/actions/explore.md +1 -1
  140. package/templates/updates/actions/onboard.md +2 -4
  141. package/templates/updates/actions/summarize.md +1 -1
  142. package/templates/updates/skills/kanban/SKILL.md +77 -3
  143. package/dist/web/.next/static/chunks/18cfbdd7e977bb01.css +0 -1
  144. package/dist/web/.next/static/chunks/a0f5f9cdee8a22c1.js +0 -4
  145. /package/dist/web/.next/static/{b2V8jC3HBMi4vgm7Kie3H → aN-jqftQVvSm0qVskLybH}/_buildManifest.js +0 -0
  146. /package/dist/web/.next/static/{b2V8jC3HBMi4vgm7Kie3H → aN-jqftQVvSm0qVskLybH}/_clientMiddlewareManifest.json +0 -0
  147. /package/dist/web/.next/static/{b2V8jC3HBMi4vgm7Kie3H → aN-jqftQVvSm0qVskLybH}/_ssgManifest.js +0 -0
@@ -0,0 +1,45 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import path from 'path';
4
+ import { getSlycodeRoot } from '@/lib/paths';
5
+ import type { ChangelogVersion } from '@/lib/types';
6
+
7
+ /**
8
+ * GET /api/changelog
9
+ *
10
+ * Returns the changelog as a JSON array of ChangelogVersion entries,
11
+ * ordered newest-first.
12
+ *
13
+ * Path resolution:
14
+ * Prod: <workspace>/node_modules/@slycode/slycode/templates/changelog.json
15
+ * Dev: <workspace>/data/changelog.json
16
+ *
17
+ * Returns an empty array on missing/malformed file (graceful degradation).
18
+ */
19
+ export async function GET() {
20
+ const root = getSlycodeRoot();
21
+
22
+ const candidatePaths = [
23
+ // Prod: shipped inside the package templates
24
+ path.join(root, 'node_modules', '@slycode', 'slycode', 'templates', 'changelog.json'),
25
+ // Dev: source of truth at data/
26
+ path.join(root, 'data', 'changelog.json'),
27
+ ];
28
+
29
+ for (const p of candidatePaths) {
30
+ if (!existsSync(p)) continue;
31
+ try {
32
+ const raw = readFileSync(p, 'utf-8');
33
+ const data = JSON.parse(raw);
34
+ if (!Array.isArray(data)) {
35
+ // Malformed shape — fall through to empty
36
+ continue;
37
+ }
38
+ return NextResponse.json(data as ChangelogVersion[]);
39
+ } catch {
40
+ // Malformed JSON — fall through to next candidate or empty
41
+ }
42
+ }
43
+
44
+ return NextResponse.json([] as ChangelogVersion[]);
45
+ }
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
- import type { KanbanBoard, KanbanStages, KanbanCard, KanbanStage } from '@/lib/types';
4
+ import type { KanbanBoard, KanbanStages, KanbanCard, KanbanStage, ChangedCard } from '@/lib/types';
5
5
  import { getNextRun } from '@/lib/scheduler';
6
6
  import { appendEvent } from '@/lib/event-log';
7
7
  import {
@@ -230,10 +230,11 @@ export async function GET(request: NextRequest) {
230
230
  export async function POST(request: NextRequest) {
231
231
  try {
232
232
  const body = await request.json();
233
- const { projectId, stages: incomingStages, changedCardIds } = body as {
233
+ const { projectId, stages: incomingStages, changedCardIds, changedCards } = body as {
234
234
  projectId: string;
235
235
  stages: KanbanStages;
236
236
  changedCardIds?: string[];
237
+ changedCards?: ChangedCard[];
237
238
  };
238
239
  let stages = incomingStages;
239
240
 
@@ -288,21 +289,60 @@ export async function POST(request: NextRequest) {
288
289
  // Clean up old numbered backups from previous system (one-time migration)
289
290
  await cleanupLegacyBackups(projectId);
290
291
 
291
- // Merge logic: when changedCardIds is present, only apply those cards from the
292
- // frontend payload and keep the disk version of everything else. This prevents
293
- // stale frontend state from overwriting concurrent CLI/agent edits.
294
- if (changedCardIds && changedCardIds.length > 0) {
292
+ // Type-aware merge: changedCards carries per-card operation types (move/edit/create/delete).
293
+ // For "move" cards, preserve disk content and overlay only positional fields.
294
+ // For "edit"/"create" cards, use the frontend version fully.
295
+ // Falls back to changedCardIds (untyped) for backward compatibility — treats all as "edit".
296
+ const effectiveChangedIds = changedCards?.map((c) => c.id) ?? changedCardIds;
297
+ if (effectiveChangedIds && effectiveChangedIds.length > 0) {
295
298
  try {
296
299
  const currentContent = await fs.readFile(kanbanPath, 'utf-8');
297
300
  const currentData = JSON.parse(currentContent) as KanbanBoard;
298
301
  const diskStages = currentData.stages || EMPTY_STAGES;
299
302
 
303
+ // Build type lookup: cardId → change type (default "edit" for backward compat)
304
+ const changeTypeMap = new Map<string, string>();
305
+ if (changedCards) {
306
+ for (const cc of changedCards) {
307
+ changeTypeMap.set(cc.id, cc.type);
308
+ }
309
+ }
310
+
311
+ // Build disk card lookup for move operations
312
+ const diskCardMap = new Map<string, KanbanCard>();
313
+ for (const [, cards] of Object.entries(diskStages) as [KanbanStage, KanbanCard[]][]) {
314
+ for (const card of cards || []) {
315
+ diskCardMap.set(card.id, card);
316
+ }
317
+ }
318
+
300
319
  // Build lookup of changed cards from frontend payload
301
- const changedCards = new Map<string, { card: KanbanCard; stage: KanbanStage }>();
320
+ const changedCardMap = new Map<string, { card: KanbanCard; stage: KanbanStage }>();
302
321
  for (const [stage, cards] of Object.entries(stages) as [KanbanStage, KanbanCard[]][]) {
303
322
  for (const card of cards || []) {
304
- if (changedCardIds.includes(card.id)) {
305
- changedCards.set(card.id, { card: { ...card, last_modified_by: 'web' }, stage });
323
+ if (effectiveChangedIds.includes(card.id)) {
324
+ const changeType = changeTypeMap.get(card.id) || 'edit';
325
+ let mergedCard: KanbanCard;
326
+
327
+ if (changeType === 'move') {
328
+ // Move: preserve disk content, overlay only positional fields
329
+ const diskCard = diskCardMap.get(card.id);
330
+ if (!diskCard) {
331
+ // Card deleted on disk during pending move — drop silently
332
+ continue;
333
+ }
334
+ mergedCard = {
335
+ ...diskCard,
336
+ order: card.order,
337
+ updated_at: card.updated_at,
338
+ last_modified_by: 'web',
339
+ };
340
+ } else {
341
+ // Edit/create: use frontend version fully
342
+ mergedCard = { ...card, last_modified_by: 'web' };
343
+ }
344
+
345
+ changedCardMap.set(card.id, { card: mergedCard, stage: stage as KanbanStage });
306
346
  }
307
347
  }
308
348
  }
@@ -317,14 +357,14 @@ export async function POST(request: NextRequest) {
317
357
  };
318
358
 
319
359
  // Remove changed cards from their current disk positions
320
- for (const cardId of changedCardIds) {
360
+ for (const cardId of effectiveChangedIds) {
321
361
  for (const stage of Object.keys(mergedStages) as KanbanStage[]) {
322
362
  mergedStages[stage] = mergedStages[stage].filter((c) => c.id !== cardId);
323
363
  }
324
364
  }
325
365
 
326
366
  // Re-add changed cards to their target stages (absent = deleted)
327
- for (const [, { card, stage }] of changedCards) {
367
+ for (const [, { card, stage }] of changedCardMap) {
328
368
  mergedStages[stage] = [...mergedStages[stage], card];
329
369
  }
330
370
 
@@ -409,7 +449,7 @@ export async function POST(request: NextRequest) {
409
449
  }
410
450
  }
411
451
 
412
- return NextResponse.json({ success: true, last_updated: data.last_updated });
452
+ return NextResponse.json({ success: true, last_updated: data.last_updated, stages: normalizedStages });
413
453
  } catch (error) {
414
454
  console.error('Failed to save kanban:', error);
415
455
  return NextResponse.json({ error: 'Failed to save' }, { status: 500 });
@@ -80,6 +80,9 @@ function renderMovedDetail(detail: string): React.ReactNode {
80
80
 
81
81
  function renderDetail(event: ActivityEvent): React.ReactNode {
82
82
  if (event.type === 'card_moved') return renderMovedDetail(event.detail);
83
+ if (typeof event.detail === 'object' && event.detail !== null) {
84
+ return JSON.stringify(event.detail);
85
+ }
83
86
  return event.detail;
84
87
  }
85
88
 
@@ -0,0 +1,115 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+
5
+ interface BranchTabProps {
6
+ projectPath: string;
7
+ isTerminalExpanded: boolean;
8
+ }
9
+
10
+ interface GitStatus {
11
+ branch: string | null;
12
+ uncommitted: number;
13
+ }
14
+
15
+ /**
16
+ * Generate a deterministic hue (0-360) from a string using DJB2 hash.
17
+ * Different branch names produce visually distinct colors.
18
+ */
19
+ function branchToHue(branch: string): number {
20
+ let hash = 5381;
21
+ for (let i = 0; i < branch.length; i++) {
22
+ hash = ((hash << 5) + hash + branch.charCodeAt(i)) >>> 0;
23
+ }
24
+ return hash % 360;
25
+ }
26
+
27
+ /**
28
+ * Detect if dark mode is active by checking for .dark class on <html>.
29
+ */
30
+ function isDarkMode(): boolean {
31
+ if (typeof document === 'undefined') return true;
32
+ return document.documentElement.classList.contains('dark');
33
+ }
34
+
35
+ export function BranchTab({ projectPath, isTerminalExpanded }: BranchTabProps) {
36
+ const [gitStatus, setGitStatus] = useState<GitStatus | null>(null);
37
+ const [dark, setDark] = useState(true);
38
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
39
+
40
+ const fetchBranch = useCallback(async () => {
41
+ try {
42
+ const res = await fetch(`/api/bridge/git-status?cwd=${encodeURIComponent(projectPath)}`);
43
+ if (!res.ok) {
44
+ setGitStatus(null);
45
+ return;
46
+ }
47
+ const data = await res.json() as GitStatus;
48
+ setGitStatus(data.branch ? data : null);
49
+ } catch {
50
+ setGitStatus(null);
51
+ }
52
+ }, [projectPath]);
53
+
54
+ // Fetch on mount + poll every 30s
55
+ useEffect(() => {
56
+ fetchBranch();
57
+ intervalRef.current = setInterval(fetchBranch, 30000);
58
+ return () => {
59
+ if (intervalRef.current) clearInterval(intervalRef.current);
60
+ };
61
+ }, [fetchBranch]);
62
+
63
+ // Watch for theme changes
64
+ useEffect(() => {
65
+ setDark(isDarkMode());
66
+ const observer = new MutationObserver(() => setDark(isDarkMode()));
67
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
68
+ return () => observer.disconnect();
69
+ }, []);
70
+
71
+ if (!gitStatus) return null;
72
+
73
+ const hue = branchToHue(gitStatus.branch!);
74
+
75
+ const bg = dark ? `hsla(${hue}, 55%, 22%, 0.9)` : `hsla(${hue}, 40%, 48%, 0.92)`;
76
+ const border = dark ? `hsla(${hue}, 60%, 45%, 0.5)` : `hsla(${hue}, 45%, 38%, 0.4)`;
77
+ const glow = dark
78
+ ? `0 -3px 12px hsla(${hue}, 70%, 50%, 0.2), 0 0 6px hsla(${hue}, 60%, 40%, 0.1)`
79
+ : `0 -2px 8px hsla(${hue}, 50%, 50%, 0.15)`;
80
+
81
+ // Position: to the left of GlobalClaudePanel
82
+ // Collapsed panel: right-4 w-64 → branch tab right = 16px + 256px + 8px gap = 280px
83
+ // Expanded panel (sm+): right-4 w-[700px] → branch tab right = 16px + 700px + 8px gap = 724px
84
+ const rightPos = isTerminalExpanded
85
+ ? 'right-4 sm:right-[724px]'
86
+ : 'right-[280px]';
87
+
88
+ return (
89
+ <div
90
+ onClick={fetchBranch}
91
+ title={`${gitStatus.branch}${gitStatus.uncommitted > 0 ? ` — ${gitStatus.uncommitted} uncommitted` : ''}\nClick to refresh`}
92
+ className={`fixed z-40 bottom-0 cursor-pointer select-none transition-all duration-300 ease-in-out ${rightPos}`}
93
+ >
94
+ <div
95
+ className="rounded-t-md px-3 py-1.5 backdrop-blur-sm flex flex-col"
96
+ style={{
97
+ background: bg,
98
+ borderWidth: '1px 1px 0 1px',
99
+ borderStyle: 'solid',
100
+ borderColor: border,
101
+ boxShadow: glow,
102
+ }}
103
+ >
104
+ <span className="max-w-[200px] truncate text-xs font-semibold text-white/90">
105
+ {gitStatus.branch}
106
+ </span>
107
+ {gitStatus.uncommitted > 0 && (
108
+ <span className="text-[10px] leading-tight text-white/55">
109
+ {gitStatus.uncommitted} uncommitted
110
+ </span>
111
+ )}
112
+ </div>
113
+ </div>
114
+ );
115
+ }
@@ -0,0 +1,234 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useCallback } from 'react';
4
+ import type { ChangelogVersion, ChangelogChangeType } from '@/lib/types';
5
+
6
+ interface ChangelogModalProps {
7
+ onClose: () => void;
8
+ }
9
+
10
+ // ============================================================================
11
+ // Type styling
12
+ // ============================================================================
13
+
14
+ const TYPE_LABELS: Record<ChangelogChangeType, string> = {
15
+ feature: 'Feature',
16
+ bugfix: 'Fix',
17
+ improvement: 'Improved',
18
+ chore: 'Chore',
19
+ };
20
+
21
+ const TYPE_STYLES: Record<ChangelogChangeType, string> = {
22
+ feature:
23
+ 'border-neon-blue-400/40 bg-neon-blue-400/15 text-neon-blue-600 dark:text-neon-blue-400',
24
+ bugfix:
25
+ 'border-red-400/40 bg-red-400/15 text-red-600 dark:text-red-400',
26
+ improvement:
27
+ 'border-emerald-400/40 bg-emerald-400/15 text-emerald-600 dark:text-emerald-400',
28
+ chore:
29
+ 'border-void-400/40 bg-void-400/15 text-void-600 dark:text-void-300',
30
+ };
31
+
32
+ // ============================================================================
33
+ // Component
34
+ // ============================================================================
35
+
36
+ export function ChangelogModal({ onClose }: ChangelogModalProps) {
37
+ const [data, setData] = useState<ChangelogVersion[] | null>(null);
38
+ const [loading, setLoading] = useState(true);
39
+ const [error, setError] = useState<string | null>(null);
40
+
41
+ // Fetch changelog on mount
42
+ useEffect(() => {
43
+ let cancelled = false;
44
+ setLoading(true);
45
+ fetch('/api/changelog')
46
+ .then((r) => {
47
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
48
+ return r.json();
49
+ })
50
+ .then((json) => {
51
+ if (cancelled) return;
52
+ setData(Array.isArray(json) ? (json as ChangelogVersion[]) : []);
53
+ setError(null);
54
+ })
55
+ .catch((err: unknown) => {
56
+ if (cancelled) return;
57
+ const message = err instanceof Error ? err.message : 'Unknown error';
58
+ setError(message);
59
+ setData([]);
60
+ })
61
+ .finally(() => {
62
+ if (cancelled) return;
63
+ setLoading(false);
64
+ });
65
+ return () => {
66
+ cancelled = true;
67
+ };
68
+ }, []);
69
+
70
+ // Escape to close
71
+ useEffect(() => {
72
+ function handleKey(e: KeyboardEvent) {
73
+ if (e.key === 'Escape') {
74
+ e.stopPropagation();
75
+ onClose();
76
+ }
77
+ }
78
+ document.addEventListener('keydown', handleKey);
79
+ return () => document.removeEventListener('keydown', handleKey);
80
+ }, [onClose]);
81
+
82
+ const formatDate = useCallback((iso: string): string => {
83
+ if (!iso) return '';
84
+ try {
85
+ const d = new Date(`${iso}T00:00:00`);
86
+ if (Number.isNaN(d.getTime())) return iso;
87
+ return d.toLocaleDateString('en-US', {
88
+ year: 'numeric',
89
+ month: 'short',
90
+ day: 'numeric',
91
+ });
92
+ } catch {
93
+ return iso;
94
+ }
95
+ }, []);
96
+
97
+ return (
98
+ <div
99
+ className="fixed inset-0 z-[55] flex items-center justify-center bg-black/60 p-4 backdrop-blur-sm"
100
+ onClick={(e) => {
101
+ if (e.target === e.currentTarget) onClose();
102
+ }}
103
+ >
104
+ <div className="flex w-full max-w-3xl max-h-[85vh] flex-col overflow-hidden rounded-xl border border-void-200 bg-white shadow-(--shadow-overlay) dark:border-void-700 dark:bg-void-850">
105
+ {/* Header */}
106
+ <div className="flex items-start justify-between border-b border-void-200 px-6 py-5 dark:border-void-800">
107
+ <div>
108
+ <div className="font-mono text-[10px] uppercase tracking-[0.22em] text-neon-blue-600 dark:text-neon-blue-400">
109
+ Release History
110
+ </div>
111
+ <h2 className="mt-1 text-2xl font-semibold tracking-tight text-void-900 dark:text-void-100">
112
+ Changelog
113
+ </h2>
114
+ </div>
115
+ <button
116
+ type="button"
117
+ onClick={onClose}
118
+ aria-label="Close changelog"
119
+ className="rounded-md p-1.5 text-void-500 transition-colors hover:bg-void-100 hover:text-void-900 dark:text-void-400 dark:hover:bg-void-800 dark:hover:text-void-100"
120
+ >
121
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden>
122
+ <path
123
+ d="M5 5l10 10M15 5L5 15"
124
+ stroke="currentColor"
125
+ strokeWidth="1.5"
126
+ strokeLinecap="round"
127
+ />
128
+ </svg>
129
+ </button>
130
+ </div>
131
+
132
+ {/* Content */}
133
+ <div className="flex-1 overflow-y-auto px-6 py-6">
134
+ {loading && (
135
+ <div className="flex items-center justify-center py-16">
136
+ <div className="font-mono text-xs uppercase tracking-widest text-void-400 dark:text-void-500">
137
+ Loading…
138
+ </div>
139
+ </div>
140
+ )}
141
+
142
+ {!loading && error && (
143
+ <div className="rounded-lg border border-red-300/60 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300">
144
+ Couldn&apos;t load the changelog: {error}
145
+ </div>
146
+ )}
147
+
148
+ {!loading && !error && data && data.length === 0 && (
149
+ <div className="flex flex-col items-center justify-center py-16 text-center">
150
+ <div className="mb-2 font-mono text-[10px] uppercase tracking-[0.22em] text-void-400 dark:text-void-600">
151
+ No entries
152
+ </div>
153
+ <p className="text-sm text-void-500 dark:text-void-400">
154
+ The changelog is empty. Future releases will appear here.
155
+ </p>
156
+ </div>
157
+ )}
158
+
159
+ {!loading && !error && data && data.length > 0 && (
160
+ <ol className="relative space-y-9">
161
+ {/* Timeline rail */}
162
+ <div
163
+ className="absolute left-[7px] top-2 bottom-2 w-px bg-gradient-to-b from-neon-blue-400/50 via-void-300 to-transparent dark:from-neon-blue-400/40 dark:via-void-700"
164
+ aria-hidden
165
+ />
166
+
167
+ {data.map((version, idx) => (
168
+ <li key={version.version} className="relative pl-8">
169
+ {/* Timeline dot */}
170
+ <div
171
+ className={`absolute left-0 top-[6px] h-[15px] w-[15px] rounded-full border-2 ${
172
+ idx === 0
173
+ ? 'border-neon-blue-400 bg-neon-blue-400/20'
174
+ : 'border-void-300 bg-white dark:border-void-600 dark:bg-void-900'
175
+ }`}
176
+ aria-hidden
177
+ >
178
+ {idx === 0 && (
179
+ <div className="absolute inset-[2px] animate-pulse rounded-full bg-neon-blue-400/70" />
180
+ )}
181
+ </div>
182
+
183
+ {/* Version header */}
184
+ <div className="mb-3 flex flex-wrap items-baseline gap-x-3 gap-y-1">
185
+ <span className="font-mono text-lg font-semibold text-void-900 dark:text-void-100">
186
+ v{version.version}
187
+ </span>
188
+ <span className="font-mono text-[11px] uppercase tracking-[0.15em] text-void-500 dark:text-void-500">
189
+ {formatDate(version.date)}
190
+ </span>
191
+ {idx === 0 && (
192
+ <span className="rounded border border-neon-blue-400/50 bg-neon-blue-400/15 px-1.5 py-[2px] font-mono text-[9px] font-medium uppercase tracking-[0.15em] text-neon-blue-600 dark:text-neon-blue-400">
193
+ Latest
194
+ </span>
195
+ )}
196
+ </div>
197
+
198
+ {/* Changes */}
199
+ <ul className="space-y-2.5">
200
+ {version.changes.map((change, ci) => (
201
+ <li key={ci} className="flex items-start gap-3">
202
+ <span
203
+ className={`mt-[2px] inline-flex w-20 shrink-0 items-center justify-center rounded border px-1.5 py-[3px] font-mono text-[10px] font-medium uppercase tracking-wider ${TYPE_STYLES[change.type]}`}
204
+ >
205
+ {TYPE_LABELS[change.type]}
206
+ </span>
207
+ <span className="text-sm leading-relaxed text-void-700 dark:text-void-300">
208
+ {change.description}
209
+ </span>
210
+ </li>
211
+ ))}
212
+ </ul>
213
+ </li>
214
+ ))}
215
+ </ol>
216
+ )}
217
+ </div>
218
+
219
+ {/* Footer */}
220
+ <div className="flex items-center justify-center gap-2 border-t border-void-200 bg-void-50 px-6 py-3 dark:border-void-800 dark:bg-void-900/50">
221
+ <span className="font-mono text-[10px] uppercase tracking-[0.18em] text-void-500 dark:text-void-500">
222
+ Press
223
+ </span>
224
+ <kbd className="rounded border border-void-300 bg-white px-1.5 py-[1px] font-mono text-[10px] text-void-700 dark:border-void-700 dark:bg-void-850 dark:text-void-300">
225
+ Esc
226
+ </kbd>
227
+ <span className="font-mono text-[10px] uppercase tracking-[0.18em] text-void-500 dark:text-void-500">
228
+ to close
229
+ </span>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ );
234
+ }
@@ -15,6 +15,7 @@ import { CliAssetsTab } from './CliAssetsTab';
15
15
  import { ActivityFeed } from './ActivityFeed';
16
16
  import { ThemeToggle } from './ThemeToggle';
17
17
  import { VersionUpdateToast } from './VersionUpdateToast';
18
+ import { ChangelogModal } from './ChangelogModal';
18
19
  import { useVoice } from '@/contexts/VoiceContext';
19
20
 
20
21
  interface DashboardProps {
@@ -35,6 +36,7 @@ export function Dashboard({ data: initialData }: DashboardProps) {
35
36
  const [dropIndex, setDropIndex] = useState<number | null>(null);
36
37
  const gridRef = useRef<HTMLDivElement>(null);
37
38
  const [slycodeVersion, setSlycodeVersion] = useState<string | null>(null);
39
+ const [showChangelog, setShowChangelog] = useState(false);
38
40
 
39
41
  // Fetch SlyCode version on mount
40
42
  useEffect(() => {
@@ -472,6 +474,13 @@ export function Dashboard({ data: initialData }: DashboardProps) {
472
474
  <p className="mt-2 text-xs text-void-500 dark:text-void-400">
473
475
  &copy; 2026 SlyCode (<a href="https://slycode.ai" target="_blank" rel="noopener noreferrer" className="hover:text-neon-blue-500 dark:hover:text-neon-blue-400 transition-colors">slycode.ai</a>). All rights reserved.
474
476
  {slycodeVersion && <span className="ml-2 text-void-400 dark:text-void-500">v{slycodeVersion}</span>}
477
+ <button
478
+ type="button"
479
+ onClick={() => setShowChangelog(true)}
480
+ className="ml-2 text-void-400 hover:text-neon-blue-500 dark:text-void-500 dark:hover:text-neon-blue-400 transition-colors underline-offset-2 hover:underline"
481
+ >
482
+ Changelog
483
+ </button>
475
484
  </p>
476
485
  </footer>
477
486
  </main>
@@ -482,6 +491,8 @@ export function Dashboard({ data: initialData }: DashboardProps) {
482
491
  onCreated={refreshData}
483
492
  />
484
493
 
494
+ {showChangelog && <ChangelogModal onClose={() => setShowChangelog(false)} />}
495
+
485
496
  {/* Global Terminal */}
486
497
  <GlobalClaudePanel
487
498
  sessionNameOverride="global:global"
@@ -8,6 +8,7 @@ import {
8
8
  import { useSlyActionsConfig } from '@/hooks/useSlyActionsConfig';
9
9
  import { onTerminalPrompt } from '@/lib/terminal-events';
10
10
  import { ClaudeTerminalPanel, type TerminalContext } from './ClaudeTerminalPanel';
11
+ import { BranchTab } from './BranchTab';
11
12
  import { useVoice } from '@/contexts/VoiceContext';
12
13
 
13
14
  interface SessionInfo {
@@ -264,6 +265,11 @@ export function GlobalClaudePanel({
264
265
  />
265
266
  </div>
266
267
  )}
268
+
269
+ {/* Git branch tab - positioned to the left of the panel */}
270
+ {cwd && (
271
+ <BranchTab projectPath={cwd} isTerminalExpanded={isExpanded} />
272
+ )}
267
273
  </div>
268
274
  );
269
275
  }