agent-canvas 0.2.1 → 0.7.0

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 (99) hide show
  1. package/README.md +42 -17
  2. package/bin/agent-canvas.js +7 -7
  3. package/bin/agent-canvas.ts +48 -37
  4. package/bin/lib/commands/daemon.ts +46 -43
  5. package/bin/lib/commands/exec.ts +100 -100
  6. package/bin/lib/commands/fetch.ts +15 -15
  7. package/bin/lib/commands/install.ts +36 -36
  8. package/bin/lib/commands/instructions.ts +51 -51
  9. package/bin/lib/commands/push.ts +115 -97
  10. package/bin/lib/commands/review.ts +190 -0
  11. package/bin/lib/commands/watch.ts +24 -24
  12. package/bin/lib/config.ts +12 -12
  13. package/bin/lib/daemon-lifecycle.ts +47 -51
  14. package/bin/lib/helpers.ts +82 -76
  15. package/bin/lib/instructions/app-review.md +88 -0
  16. package/bin/lib/instructions/component-checklist.md +7 -5
  17. package/bin/lib/instructions/component-custom.md +20 -14
  18. package/bin/lib/instructions/component-diff.md +4 -3
  19. package/bin/lib/instructions/component-item.md +18 -8
  20. package/bin/lib/instructions/component-markdown.md +4 -2
  21. package/bin/lib/instructions/component-mermaid.md +12 -6
  22. package/bin/lib/instructions/component-multichoice.md +6 -2
  23. package/bin/lib/instructions/component-note.md +1 -1
  24. package/bin/lib/instructions/component-priority.md +1 -1
  25. package/bin/lib/instructions/component-rangeinput.md +9 -2
  26. package/bin/lib/instructions/component-secretinput.md +12 -11
  27. package/bin/lib/instructions/component-svg.md +62 -16
  28. package/bin/lib/instructions/component-table.md +6 -6
  29. package/bin/lib/instructions/component-usefeedback.md +8 -8
  30. package/bin/lib/instructions/component-userinput.md +6 -2
  31. package/bin/lib/instructions/flow-feature.md +22 -5
  32. package/bin/lib/instructions/flow-plan.md +5 -5
  33. package/bin/lib/instructions/flow-runbook.md +28 -25
  34. package/bin/lib/instructions/overview.md +27 -18
  35. package/bin/lib/instructions/theming.md +5 -0
  36. package/daemon/build.ts +171 -152
  37. package/daemon/client/App.tsx +1205 -1077
  38. package/daemon/client/CompareView.tsx +478 -458
  39. package/daemon/client/FileBrowser.tsx +241 -203
  40. package/daemon/client/FileViewer.tsx +220 -211
  41. package/daemon/client/OverviewView.tsx +283 -267
  42. package/daemon/client/ReviewerIdentityDialog.tsx +72 -71
  43. package/daemon/client/RevisionSelect.tsx +111 -95
  44. package/daemon/client/SessionSwitcher.tsx +81 -81
  45. package/daemon/client/ShareDialog.tsx +211 -212
  46. package/daemon/client/apiSchemas.ts +78 -0
  47. package/daemon/client/canvas-runtime.ts +1 -1
  48. package/daemon/client/clientApi.ts +194 -206
  49. package/daemon/client/components/SecretInput.tsx +138 -138
  50. package/daemon/client/components/index.ts +2 -2
  51. package/daemon/client/domDiff.ts +341 -333
  52. package/daemon/client/errorMessage.ts +3 -0
  53. package/daemon/client/exportMarkdown.ts +222 -201
  54. package/daemon/client/lcs.ts +19 -14
  55. package/daemon/client/shareCrypto.ts +57 -57
  56. package/daemon/client/unifiedDiff.ts +660 -649
  57. package/daemon/dist/annotations-runtime.js +1 -1
  58. package/daemon/dist/client.css +2 -2
  59. package/daemon/dist/client.js +77 -80
  60. package/daemon/dist/components.js +59 -59
  61. package/daemon/dist/preact-compat.js +1 -1
  62. package/daemon/dist/review.js +361 -0
  63. package/daemon/dist/runtime.js +1 -1
  64. package/daemon/langMap.ts +24 -6
  65. package/daemon/review-widget/anchor.ts +187 -0
  66. package/daemon/review-widget/client.ts +95 -0
  67. package/daemon/review-widget/index.tsx +36 -0
  68. package/daemon/review-widget/markup.ts +71 -0
  69. package/daemon/review-widget/screenshot.ts +91 -0
  70. package/daemon/review-widget/source.ts +103 -0
  71. package/daemon/review-widget/styles.css +359 -0
  72. package/daemon/review-widget/text-modules.d.ts +4 -0
  73. package/daemon/review-widget/tsconfig.json +16 -0
  74. package/daemon/review-widget/widget.tsx +436 -0
  75. package/daemon/src/components.ts +3 -3
  76. package/daemon/src/handlers/api.ts +330 -328
  77. package/daemon/src/handlers/files.ts +112 -106
  78. package/daemon/src/handlers/static.ts +47 -46
  79. package/daemon/src/handlers/uploads.ts +61 -56
  80. package/daemon/src/paths.ts +13 -10
  81. package/daemon/src/remote-feedback.ts +192 -192
  82. package/daemon/src/review/markdown.ts +52 -0
  83. package/daemon/src/review/model.ts +132 -0
  84. package/daemon/src/review/proxy.ts +177 -0
  85. package/daemon/src/review/server.ts +199 -0
  86. package/daemon/src/review/store.ts +151 -0
  87. package/daemon/src/server.ts +127 -86
  88. package/daemon/src/share.ts +163 -169
  89. package/daemon/src/stubFetch.ts +7 -0
  90. package/daemon/tsconfig.json +19 -19
  91. package/package.json +47 -47
  92. package/skills/canvas/SKILL.md +5 -1
  93. package/bin/lib/commands/exec.test.ts +0 -27
  94. package/bin/lib/helpers.test.ts +0 -79
  95. package/daemon/src/files-api.test.ts +0 -78
  96. package/daemon/src/remote-feedback.test.ts +0 -165
  97. package/daemon/src/secrets-api.test.ts +0 -123
  98. package/daemon/src/share-e2e.test.ts +0 -277
  99. package/daemon/src/share.test.ts +0 -289
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useRef } from "react";
1
+ import { useEffect, useRef, useState } from 'react'
2
2
 
3
3
  /**
4
4
  * Modal shown in shared mode when a reviewer submits feedback without a
@@ -11,85 +11,86 @@ import React, { useState, useEffect, useRef } from "react";
11
11
  * the same reviewer across revisions.
12
12
  */
13
13
  interface ReviewerIdentityDialogProps {
14
- open: boolean;
15
- onClose: () => void;
16
- onSubmit: (name: string) => void;
14
+ open: boolean
15
+ onClose: () => void
16
+ onSubmit: (name: string) => void
17
17
  }
18
18
 
19
19
  export function ReviewerIdentityDialog({ open, onClose, onSubmit }: ReviewerIdentityDialogProps) {
20
- const [name, setName] = useState("");
21
- const inputRef = useRef<HTMLInputElement>(null);
20
+ const [name, setName] = useState('')
21
+ const inputRef = useRef<HTMLInputElement>(null)
22
22
 
23
- useEffect(() => {
24
- if (open) {
25
- setName("");
26
- setTimeout(() => inputRef.current?.focus(), 50);
27
- }
28
- }, [open]);
23
+ useEffect(() => {
24
+ if (open) {
25
+ setName('')
26
+ setTimeout(() => inputRef.current?.focus(), 50)
27
+ }
28
+ }, [open])
29
29
 
30
- if (!open) return null;
30
+ if (!open) return null
31
31
 
32
- const trimmed = name.trim();
33
- const valid = trimmed.length >= 1 && trimmed.length <= 80;
32
+ const trimmed = name.trim()
33
+ const valid = trimmed.length >= 1 && trimmed.length <= 80
34
34
 
35
- const handleSubmit = () => {
36
- if (!valid) return;
37
- onSubmit(trimmed);
38
- };
35
+ const handleSubmit = () => {
36
+ if (!valid) return
37
+ onSubmit(trimmed)
38
+ }
39
39
 
40
- return (
41
- <div
42
- className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
43
- onClick={onClose}
44
- >
45
- <div
46
- className="bg-bg-surface border border-border-medium rounded-lg shadow-xl w-full max-w-md mx-4 p-5"
47
- onClick={(e) => e.stopPropagation()}
48
- >
49
- <div className="flex items-center justify-between mb-3">
50
- <h2 className="text-sm font-medium text-text-primary font-body">
51
- Your name
52
- </h2>
53
- <button
54
- onClick={onClose}
55
- className="w-6 h-6 flex items-center justify-center rounded text-text-tertiary hover:text-text-secondary hover:bg-bg-elevated transition-colors"
56
- >
57
- <span className="text-xs">&#x2715;</span>
58
- </button>
59
- </div>
40
+ return (
41
+ <div
42
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
43
+ onClick={onClose}
44
+ >
45
+ <div
46
+ className="bg-bg-surface border border-border-medium rounded-lg shadow-xl w-full max-w-md mx-4 p-5"
47
+ onClick={(e) => e.stopPropagation()}
48
+ >
49
+ <div className="flex items-center justify-between mb-3">
50
+ <h2 className="text-sm font-medium text-text-primary font-body">
51
+ Your name
52
+ </h2>
53
+ <button
54
+ onClick={onClose}
55
+ className="w-6 h-6 flex items-center justify-center rounded text-text-tertiary hover:text-text-secondary hover:bg-bg-elevated transition-colors"
56
+ >
57
+ <span className="text-xs">&#x2715;</span>
58
+ </button>
59
+ </div>
60
60
 
61
- <p className="text-[13px] text-text-secondary font-body mb-3 leading-relaxed">
62
- Before you leave feedback, please enter a name so the canvas author
63
- knows who reviewed it. This is stored locally in your browser.
64
- </p>
61
+ <p className="text-[13px] text-text-secondary font-body mb-3 leading-relaxed">
62
+ Before you leave feedback, please enter a name so the canvas author knows who reviewed it. This is stored locally in your browser.
63
+ </p>
65
64
 
66
- <input
67
- ref={inputRef}
68
- type="text"
69
- value={name}
70
- onChange={(e) => setName(e.target.value)}
71
- onKeyDown={(e) => { if (e.key === "Enter") handleSubmit(); }}
72
- maxLength={80}
73
- placeholder="e.g. Alice"
74
- className="w-full px-3 py-2 text-[13px] font-body bg-bg-input border border-border-medium rounded text-text-primary placeholder:text-text-disabled focus:outline-none focus:border-accent-blue"
75
- />
65
+ <input
66
+ ref={inputRef}
67
+ type="text"
68
+ value={name}
69
+ onChange={(e) => setName(e.target.value)}
70
+ onKeyDown={(e) => {
71
+ if (e.key === 'Enter') handleSubmit()
72
+ }}
73
+ maxLength={80}
74
+ placeholder="e.g. Alice"
75
+ className="w-full px-3 py-2 text-[13px] font-body bg-bg-input border border-border-medium rounded text-text-primary placeholder:text-text-disabled focus:outline-none focus:border-accent-blue"
76
+ />
76
77
 
77
- <div className="flex justify-end gap-2 mt-4">
78
- <button
79
- onClick={onClose}
80
- className="px-3 py-1.5 text-[12px] font-body font-medium rounded text-text-secondary hover:text-text-primary hover:bg-bg-elevated transition-colors"
81
- >
82
- Cancel
83
- </button>
84
- <button
85
- onClick={handleSubmit}
86
- disabled={!valid}
87
- className="px-3 py-1.5 text-[12px] font-body font-medium rounded bg-accent-blue text-white hover:opacity-90 transition-opacity disabled:opacity-50"
88
- >
89
- Submit feedback
90
- </button>
91
- </div>
92
- </div>
93
- </div>
94
- );
78
+ <div className="flex justify-end gap-2 mt-4">
79
+ <button
80
+ onClick={onClose}
81
+ className="px-3 py-1.5 text-[12px] font-body font-medium rounded text-text-secondary hover:text-text-primary hover:bg-bg-elevated transition-colors"
82
+ >
83
+ Cancel
84
+ </button>
85
+ <button
86
+ onClick={handleSubmit}
87
+ disabled={!valid}
88
+ className="px-3 py-1.5 text-[12px] font-body font-medium rounded bg-accent-blue text-white hover:opacity-90 transition-opacity disabled:opacity-50"
89
+ >
90
+ Submit feedback
91
+ </button>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ )
95
96
  }
@@ -1,113 +1,129 @@
1
- import React, { useEffect, useState, useRef, useContext } from "react";
2
- import { RevisionContext, RevisionInfo } from "@fabrika/canvas-kernel/client";
1
+ import { RevisionContext, RevisionInfo } from '@fabrika/canvas-kernel/client'
2
+ import { useContext, useEffect, useRef, useState } from 'react'
3
3
 
4
4
  /** Format a revision for the trigger button: label or "Round N" */
5
5
  function revisionButtonLabel(r: RevisionInfo) {
6
- return r.label || `Round ${r.revision}`;
6
+ return r.label || `Round ${r.revision}`
7
7
  }
8
8
 
9
9
  /** Format relative time from ISO string */
10
10
  function relativeTime(iso: string): string {
11
- const diff = Date.now() - new Date(iso).getTime();
12
- const seconds = Math.floor(diff / 1000);
13
- if (seconds < 60) return "just now";
14
- const minutes = Math.floor(seconds / 60);
15
- if (minutes < 60) return `${minutes}m ago`;
16
- const hours = Math.floor(minutes / 60);
17
- if (hours < 24) return `${hours}h ago`;
18
- const days = Math.floor(hours / 24);
19
- return `${days}d ago`;
11
+ const diff = Date.now() - new Date(iso).getTime()
12
+ const seconds = Math.floor(diff / 1000)
13
+ if (seconds < 60) return 'just now'
14
+ const minutes = Math.floor(seconds / 60)
15
+ if (minutes < 60) return `${minutes}m ago`
16
+ const hours = Math.floor(minutes / 60)
17
+ if (hours < 24) return `${hours}h ago`
18
+ const days = Math.floor(hours / 24)
19
+ return `${days}d ago`
20
20
  }
21
21
 
22
22
  interface RevisionSelectProps {
23
- value: number;
24
- onChange: (rev: number) => void;
25
- /** Accent color for the trigger text */
26
- accent?: "default" | "red" | "green" | "amber";
27
- /** Extra class on the trigger button */
28
- className?: string;
23
+ value: number
24
+ onChange: (rev: number) => void
25
+ /** Accent color for the trigger text */
26
+ accent?: 'default' | 'red' | 'green' | 'amber'
27
+ /** Extra class on the trigger button */
28
+ className?: string
29
29
  }
30
30
 
31
- export function RevisionSelect({ value, onChange, accent = "default", className }: RevisionSelectProps) {
32
- const { currentRevision, revisions } = useContext(RevisionContext);
33
- const [open, setOpen] = useState(false);
34
- const ref = useRef<HTMLDivElement>(null);
31
+ export function RevisionSelect({ value, onChange, accent = 'default', className }: RevisionSelectProps) {
32
+ const { currentRevision, revisions } = useContext(RevisionContext)
33
+ const [open, setOpen] = useState(false)
34
+ const ref = useRef<HTMLDivElement>(null)
35
35
 
36
- useEffect(() => {
37
- if (!open) return;
38
- const handler = (e: MouseEvent) => {
39
- if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
40
- };
41
- document.addEventListener("mousedown", handler);
42
- return () => document.removeEventListener("mousedown", handler);
43
- }, [open]);
36
+ useEffect(() => {
37
+ if (!open) return
38
+ const handler = (e: MouseEvent) => {
39
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
40
+ }
41
+ document.addEventListener('mousedown', handler)
42
+ return () => document.removeEventListener('mousedown', handler)
43
+ }, [open])
44
44
 
45
- const selected = revisions.find((r) => r.revision === value);
45
+ const selected = revisions.find((r) => r.revision === value)
46
46
 
47
- const accentClasses = {
48
- default: "text-text-secondary hover:text-text-primary",
49
- red: "text-accent-red",
50
- green: "text-accent-green",
51
- amber: "text-accent-amber bg-highlight-selected",
52
- };
47
+ const accentClasses = {
48
+ default: 'text-text-secondary hover:text-text-primary',
49
+ red: 'text-accent-red',
50
+ green: 'text-accent-green',
51
+ amber: 'text-accent-amber bg-highlight-selected',
52
+ }
53
53
 
54
- return (
55
- <div className="relative" ref={ref}>
56
- <button
57
- onClick={() => setOpen(!open)}
58
- className={`flex items-center gap-1.5 text-[12px] font-body font-medium px-2 py-1 rounded-md transition-colors hover:bg-bg-elevated ${accentClasses[accent]} ${className || ""}`}
59
- >
60
- {selected ? revisionButtonLabel(selected) : `Round ${value}`}
61
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none" className={`text-text-tertiary transition-transform duration-200 ${open ? "rotate-180" : ""}`}>
62
- <path d="M2.5 4l2.5 2.5L7.5 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
63
- </svg>
64
- </button>
54
+ return (
55
+ <div className="relative" ref={ref}>
56
+ <button
57
+ onClick={() => setOpen(!open)}
58
+ className={`flex items-center gap-1.5 text-[12px] font-body font-medium px-2 py-1 rounded-md transition-colors hover:bg-bg-elevated ${
59
+ accentClasses[accent]
60
+ } ${className || ''}`}
61
+ >
62
+ {selected ? revisionButtonLabel(selected) : `Round ${value}`}
63
+ <svg
64
+ width="10"
65
+ height="10"
66
+ viewBox="0 0 10 10"
67
+ fill="none"
68
+ className={`text-text-tertiary transition-transform duration-200 ${open ? 'rotate-180' : ''}`}
69
+ >
70
+ <path d="M2.5 4l2.5 2.5L7.5 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
71
+ </svg>
72
+ </button>
65
73
 
66
- {open && (
67
- <div className="absolute top-full left-0 mt-1 w-64 bg-bg-elevated border border-border-hover rounded-lg shadow-lg z-50 py-1 max-h-80 overflow-y-auto">
68
- {[...revisions].reverse().map((r) => (
69
- <button
70
- key={r.revision}
71
- onClick={() => { onChange(r.revision); setOpen(false); }}
72
- className={`w-full flex items-center gap-2 px-3 py-2 text-[12px] font-body transition-colors ${
73
- r.revision === value
74
- ? "bg-bg-surface text-text-primary"
75
- : "text-text-secondary hover:bg-bg-surface hover:text-text-primary"
76
- }`}
77
- >
78
- <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
79
- r.revision === currentRevision ? "bg-accent-green" : r.hasFeedback ? "bg-accent-amber" : "bg-border-hover"
80
- }`} />
81
- <span className="flex-1 text-left truncate">
82
- {r.label ? (
83
- <>{r.label} <span className="text-text-tertiary">#{r.revision}</span></>
84
- ) : (
85
- <>Round {r.revision}</>
86
- )}
87
- </span>
88
- <span className="text-[10px] text-text-tertiary flex-shrink-0 flex items-center gap-1.5">
89
- {(() => {
90
- const stats = r.canvasFiles?.reduce((acc, cf) => {
91
- if (cf.diffStats) { acc.added += cf.diffStats.added; acc.removed += cf.diffStats.removed; }
92
- return acc;
93
- }, { added: 0, removed: 0 });
94
- if (stats && (stats.added > 0 || stats.removed > 0)) {
95
- return (
96
- <span className="font-mono">
97
- <span className="text-accent-green">+{stats.added}</span>
98
- {" "}
99
- <span className="text-accent-red">-{stats.removed}</span>
100
- </span>
101
- );
102
- }
103
- return null;
104
- })()}
105
- {r.revision === currentRevision ? "current" : r.hasFeedback ? "sent" : relativeTime(r.createdAt)}
106
- </span>
107
- </button>
108
- ))}
109
- </div>
110
- )}
111
- </div>
112
- );
74
+ {open && (
75
+ <div className="absolute top-full left-0 mt-1 w-64 bg-bg-elevated border border-border-hover rounded-lg shadow-lg z-50 py-1 max-h-80 overflow-y-auto">
76
+ {[...revisions].reverse().map((r) => (
77
+ <button
78
+ key={r.revision}
79
+ onClick={() => {
80
+ onChange(r.revision)
81
+ setOpen(false)
82
+ }}
83
+ className={`w-full flex items-center gap-2 px-3 py-2 text-[12px] font-body transition-colors ${
84
+ r.revision === value
85
+ ? 'bg-bg-surface text-text-primary'
86
+ : 'text-text-secondary hover:bg-bg-surface hover:text-text-primary'
87
+ }`}
88
+ >
89
+ <span
90
+ className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
91
+ r.revision === currentRevision ? 'bg-accent-green' : r.hasFeedback ? 'bg-accent-amber' : 'bg-border-hover'
92
+ }`}
93
+ />
94
+ <span className="flex-1 text-left truncate">
95
+ {r.label
96
+ ? (
97
+ <>
98
+ {r.label} <span className="text-text-tertiary">#{r.revision}</span>
99
+ </>
100
+ )
101
+ : <>Round {r.revision}</>}
102
+ </span>
103
+ <span className="text-[10px] text-text-tertiary flex-shrink-0 flex items-center gap-1.5">
104
+ {(() => {
105
+ const stats = r.canvasFiles?.reduce((acc, cf) => {
106
+ if (cf.diffStats) {
107
+ acc.added += cf.diffStats.added
108
+ acc.removed += cf.diffStats.removed
109
+ }
110
+ return acc
111
+ }, { added: 0, removed: 0 })
112
+ if (stats && (stats.added > 0 || stats.removed > 0)) {
113
+ return (
114
+ <span className="font-mono">
115
+ <span className="text-accent-green">+{stats.added}</span> <span className="text-accent-red">-{stats.removed}</span>
116
+ </span>
117
+ )
118
+ }
119
+ return null
120
+ })()}
121
+ {r.revision === currentRevision ? 'current' : r.hasFeedback ? 'sent' : relativeTime(r.createdAt)}
122
+ </span>
123
+ </button>
124
+ ))}
125
+ </div>
126
+ )}
127
+ </div>
128
+ )
113
129
  }
@@ -1,98 +1,98 @@
1
- import React, { useEffect, useState, useRef } from "react";
2
- import { FS_AVAILABLE } from "./clientApi";
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { FS_AVAILABLE } from './clientApi'
3
3
 
4
4
  interface SessionInfo {
5
- id: string;
6
- projectRoot?: string;
7
- currentRevision: number;
8
- updatedAt: string;
5
+ id: string
6
+ projectRoot?: string
7
+ currentRevision: number
8
+ updatedAt: string
9
9
  }
10
10
 
11
11
  function sessionDisplayName(id: string, projectRoot?: string): string {
12
- if (projectRoot) {
13
- const parts = projectRoot.replace(/\/+$/, "").split("/");
14
- return parts[parts.length - 1] || id;
15
- }
16
- return id.length > 30 ? id.slice(0, 30) + "..." : id;
12
+ if (projectRoot) {
13
+ const parts = projectRoot.replace(/\/+$/, '').split('/')
14
+ return parts[parts.length - 1] || id
15
+ }
16
+ return id.length > 30 ? id.slice(0, 30) + '...' : id
17
17
  }
18
18
 
19
19
  interface SessionSwitcherProps {
20
- currentSessionId: string;
21
- projectRoot?: string;
20
+ currentSessionId: string
21
+ projectRoot?: string
22
22
  }
23
23
 
24
24
  export function SessionSwitcher({ currentSessionId, projectRoot }: SessionSwitcherProps) {
25
- const [sessions, setSessions] = useState<SessionInfo[]>([]);
26
- const [open, setOpen] = useState(false);
27
- const ref = useRef<HTMLDivElement>(null);
25
+ const [sessions, setSessions] = useState<SessionInfo[]>([])
26
+ const [open, setOpen] = useState(false)
27
+ const ref = useRef<HTMLDivElement>(null)
28
28
 
29
- useEffect(() => {
30
- // /api/sessions only exists on the local daemon. In shared mode we skip
31
- // the switcher entirely (it's hidden by the parent), but guard the poll
32
- // just in case this component is mounted — nothing to switch to.
33
- if (!FS_AVAILABLE) return;
34
- const fetchSessions = async () => {
35
- try {
36
- const res = await fetch("/api/sessions");
37
- const data = await res.json() as SessionInfo[];
38
- setSessions((prev) => {
39
- const next = JSON.stringify(data);
40
- return JSON.stringify(prev) === next ? prev : data;
41
- });
42
- } catch {}
43
- };
44
- fetchSessions();
45
- const interval = setInterval(fetchSessions, 5000);
46
- return () => clearInterval(interval);
47
- }, []);
29
+ useEffect(() => {
30
+ // /api/sessions only exists on the local daemon. In shared mode we skip
31
+ // the switcher entirely (it's hidden by the parent), but guard the poll
32
+ // just in case this component is mounted — nothing to switch to.
33
+ if (!FS_AVAILABLE) return
34
+ const fetchSessions = async () => {
35
+ try {
36
+ const res = await fetch('/api/sessions')
37
+ const data = await res.json() as SessionInfo[]
38
+ setSessions((prev) => {
39
+ const next = JSON.stringify(data)
40
+ return JSON.stringify(prev) === next ? prev : data
41
+ })
42
+ } catch {}
43
+ }
44
+ fetchSessions()
45
+ const interval = setInterval(fetchSessions, 5000)
46
+ return () => clearInterval(interval)
47
+ }, [])
48
48
 
49
- useEffect(() => {
50
- if (!open) return;
51
- const handler = (e: MouseEvent) => {
52
- if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
53
- };
54
- document.addEventListener("mousedown", handler);
55
- return () => document.removeEventListener("mousedown", handler);
56
- }, [open]);
49
+ useEffect(() => {
50
+ if (!open) return
51
+ const handler = (e: MouseEvent) => {
52
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
53
+ }
54
+ document.addEventListener('mousedown', handler)
55
+ return () => document.removeEventListener('mousedown', handler)
56
+ }, [open])
57
57
 
58
- const otherSessions = sessions.filter((s) => s.id !== currentSessionId);
59
- const displayName = sessionDisplayName(currentSessionId, projectRoot);
58
+ const otherSessions = sessions.filter((s) => s.id !== currentSessionId)
59
+ const displayName = sessionDisplayName(currentSessionId, projectRoot)
60
60
 
61
- return (
62
- <div className="relative" ref={ref}>
63
- <div className="flex items-center gap-1.5 font-body text-body font-medium">
64
- <a
65
- href={`/s/${currentSessionId}`}
66
- className="text-text-primary hover:text-text-secondary transition-colors"
67
- >
68
- {displayName}
69
- </a>
70
- {otherSessions.length > 0 && (
71
- <button
72
- onClick={() => setOpen(!open)}
73
- className="flex items-center justify-center text-text-tertiary hover:text-text-secondary transition-colors"
74
- >
75
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none" className={`transition-transform duration-200 ${open ? "rotate-180" : ""}`}>
76
- <path d="M3 5l3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
77
- </svg>
78
- </button>
79
- )}
80
- </div>
61
+ return (
62
+ <div className="relative" ref={ref}>
63
+ <div className="flex items-center gap-1.5 font-body text-body font-medium">
64
+ <a
65
+ href={`/s/${currentSessionId}`}
66
+ className="text-text-primary hover:text-text-secondary transition-colors"
67
+ >
68
+ {displayName}
69
+ </a>
70
+ {otherSessions.length > 0 && (
71
+ <button
72
+ onClick={() => setOpen(!open)}
73
+ className="flex items-center justify-center text-text-tertiary hover:text-text-secondary transition-colors"
74
+ >
75
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" className={`transition-transform duration-200 ${open ? 'rotate-180' : ''}`}>
76
+ <path d="M3 5l3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
77
+ </svg>
78
+ </button>
79
+ )}
80
+ </div>
81
81
 
82
- {open && otherSessions.length > 0 && (
83
- <div className="absolute top-full left-0 mt-2 w-64 bg-bg-elevated border border-border-hover rounded-lg shadow-lg z-50 py-1 overflow-hidden">
84
- {otherSessions.map((s) => (
85
- <a
86
- key={s.id}
87
- href={`/s/${s.id}`}
88
- className="flex items-center gap-2.5 px-4 py-2.5 text-body font-body text-text-secondary hover:bg-bg-surface hover:text-text-primary transition-colors"
89
- >
90
- <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${s.currentRevision > 0 ? "bg-accent-green" : "bg-border-hover"}`} />
91
- <span className="truncate">{sessionDisplayName(s.id, s.projectRoot)}</span>
92
- </a>
93
- ))}
94
- </div>
95
- )}
96
- </div>
97
- );
82
+ {open && otherSessions.length > 0 && (
83
+ <div className="absolute top-full left-0 mt-2 w-64 bg-bg-elevated border border-border-hover rounded-lg shadow-lg z-50 py-1 overflow-hidden">
84
+ {otherSessions.map((s) => (
85
+ <a
86
+ key={s.id}
87
+ href={`/s/${s.id}`}
88
+ className="flex items-center gap-2.5 px-4 py-2.5 text-body font-body text-text-secondary hover:bg-bg-surface hover:text-text-primary transition-colors"
89
+ >
90
+ <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${s.currentRevision > 0 ? 'bg-accent-green' : 'bg-border-hover'}`} />
91
+ <span className="truncate">{sessionDisplayName(s.id, s.projectRoot)}</span>
92
+ </a>
93
+ ))}
94
+ </div>
95
+ )}
96
+ </div>
97
+ )
98
98
  }