upfynai-code 0.1.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 (65) hide show
  1. package/LICENSE +22 -0
  2. package/bin/cli.js +86 -0
  3. package/dist/assets/CanvasPanel-B48gAKVY.js +538 -0
  4. package/dist/assets/CanvasPanel-B48gAKVY.js.map +1 -0
  5. package/dist/assets/CanvasPanel-BsOG3EVs.css +1 -0
  6. package/dist/assets/index-CEhTwG68.css +1 -0
  7. package/dist/assets/index-GqAGWpJI.js +70 -0
  8. package/dist/assets/index-GqAGWpJI.js.map +1 -0
  9. package/dist/index.html +18 -0
  10. package/index.html +17 -0
  11. package/package.json +67 -0
  12. package/src/App.tsx +226 -0
  13. package/src/components/canvas/CanvasPanel.tsx +62 -0
  14. package/src/components/canvas/layout/graph-builder.ts +136 -0
  15. package/src/components/canvas/shapes/CompactionNodeShape.tsx +76 -0
  16. package/src/components/canvas/shapes/SessionNodeShape.tsx +93 -0
  17. package/src/components/canvas/shapes/StatuslineWidgetShape.tsx +125 -0
  18. package/src/components/canvas/shapes/TextResponseNodeShape.tsx +86 -0
  19. package/src/components/canvas/shapes/ToolCallNodeShape.tsx +107 -0
  20. package/src/components/canvas/shapes/ToolResultNodeShape.tsx +87 -0
  21. package/src/components/canvas/shapes/shared-styles.ts +35 -0
  22. package/src/components/chat/ChatPanel.tsx +96 -0
  23. package/src/components/chat/InputBar.tsx +81 -0
  24. package/src/components/chat/MessageList.tsx +130 -0
  25. package/src/components/chat/PermissionDialog.tsx +70 -0
  26. package/src/components/layout/FolderSelector.tsx +152 -0
  27. package/src/components/layout/ModelSelector.tsx +65 -0
  28. package/src/components/layout/SessionManager.tsx +115 -0
  29. package/src/components/statusline/StatuslineBar.tsx +114 -0
  30. package/src/main.tsx +10 -0
  31. package/src/server/claude-session.ts +156 -0
  32. package/src/server/index.ts +149 -0
  33. package/src/services/stream-consumer.ts +330 -0
  34. package/src/statusline-core/bin/statusline.sh +121 -0
  35. package/src/statusline-core/commands/sls-config.md +42 -0
  36. package/src/statusline-core/commands/sls-doctor.md +35 -0
  37. package/src/statusline-core/commands/sls-help.md +48 -0
  38. package/src/statusline-core/commands/sls-layout.md +38 -0
  39. package/src/statusline-core/commands/sls-preview.md +34 -0
  40. package/src/statusline-core/commands/sls-theme.md +40 -0
  41. package/src/statusline-core/installer.js +228 -0
  42. package/src/statusline-core/layouts/compact.sh +21 -0
  43. package/src/statusline-core/layouts/full.sh +62 -0
  44. package/src/statusline-core/layouts/standard.sh +39 -0
  45. package/src/statusline-core/lib/core.sh +389 -0
  46. package/src/statusline-core/lib/helpers.sh +81 -0
  47. package/src/statusline-core/lib/json-parser.sh +71 -0
  48. package/src/statusline-core/themes/catppuccin.sh +32 -0
  49. package/src/statusline-core/themes/default.sh +37 -0
  50. package/src/statusline-core/themes/gruvbox.sh +32 -0
  51. package/src/statusline-core/themes/nord.sh +32 -0
  52. package/src/statusline-core/themes/tokyo-night.sh +32 -0
  53. package/src/store/canvas-store.ts +50 -0
  54. package/src/store/chat-store.ts +60 -0
  55. package/src/store/permission-store.ts +29 -0
  56. package/src/store/session-store.ts +52 -0
  57. package/src/store/statusline-store.ts +160 -0
  58. package/src/styles/global.css +117 -0
  59. package/src/themes/index.ts +149 -0
  60. package/src/types/canvas-graph.ts +24 -0
  61. package/src/types/sdk-messages.ts +156 -0
  62. package/src/types/statusline-fields.ts +67 -0
  63. package/src/vite-env.d.ts +1 -0
  64. package/tsconfig.json +26 -0
  65. package/vite.config.ts +24 -0
@@ -0,0 +1,81 @@
1
+ import { useRef, useCallback } from 'react';
2
+ import { useChatStore } from '../../store/chat-store';
3
+ import { useSessionStore } from '../../store/session-store';
4
+ import { useThemeStore } from '../../themes';
5
+ import { sendPrompt } from '../../services/stream-consumer';
6
+
7
+ export function InputBar() {
8
+ const theme = useThemeStore((s) => s.activeTheme);
9
+ const input = useChatStore((s) => s.input);
10
+ const isWaiting = useChatStore((s) => s.isWaiting);
11
+ const isStreaming = useSessionStore((s) => s.isStreaming);
12
+ const setInput = useChatStore((s) => s.setInput);
13
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
14
+
15
+ const handleSend = useCallback(() => {
16
+ const text = input.trim();
17
+ if (!text || isWaiting || isStreaming) return;
18
+ sendPrompt(text);
19
+ }, [input, isWaiting, isStreaming]);
20
+
21
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
22
+ if (e.key === 'Enter' && !e.shiftKey) {
23
+ e.preventDefault();
24
+ handleSend();
25
+ }
26
+ }, [handleSend]);
27
+
28
+ return (
29
+ <div style={{
30
+ padding: '8px 16px', borderTop: `1px solid ${theme.colors.border}`,
31
+ background: theme.colors.bgSurface, flexShrink: 0,
32
+ }}>
33
+ <div style={{
34
+ display: 'flex', gap: 8, alignItems: 'flex-end',
35
+ background: theme.colors.bgPanel, borderRadius: 8,
36
+ border: `1px solid ${theme.colors.border}`, padding: '8px 12px',
37
+ }}>
38
+ <textarea
39
+ ref={textareaRef}
40
+ value={input}
41
+ onChange={(e) => {
42
+ setInput(e.target.value);
43
+ // Auto-resize
44
+ e.target.style.height = 'auto';
45
+ e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
46
+ }}
47
+ onKeyDown={handleKeyDown}
48
+ placeholder={isWaiting ? 'Waiting for response...' : 'Message Claude Code...'}
49
+ disabled={isWaiting || isStreaming}
50
+ rows={1}
51
+ style={{
52
+ flex: 1, background: 'transparent', border: 'none', outline: 'none',
53
+ color: theme.colors.text, fontFamily: 'var(--font-sans)', fontSize: 14,
54
+ resize: 'none', lineHeight: 1.5, maxHeight: 200,
55
+ }}
56
+ />
57
+ <button
58
+ onClick={handleSend}
59
+ disabled={!input.trim() || isWaiting || isStreaming}
60
+ style={{
61
+ background: input.trim() && !isWaiting ? theme.colors.accent : theme.colors.bgHover,
62
+ color: input.trim() && !isWaiting ? '#fff' : theme.colors.textDim,
63
+ border: 'none', borderRadius: 6, padding: '6px 16px',
64
+ fontSize: 13, fontWeight: 600, cursor: input.trim() && !isWaiting ? 'pointer' : 'default',
65
+ transition: 'background 0.15s',
66
+ flexShrink: 0,
67
+ }}
68
+ >
69
+ {isWaiting || isStreaming ? '...' : 'Send'}
70
+ </button>
71
+ </div>
72
+ <div style={{
73
+ display: 'flex', justifyContent: 'space-between', marginTop: 4,
74
+ fontSize: 11, color: theme.colors.textDim, padding: '0 4px',
75
+ }}>
76
+ <span>Enter to send, Shift+Enter for newline</span>
77
+ <span>Powered by Claude Code SDK</span>
78
+ </div>
79
+ </div>
80
+ );
81
+ }
@@ -0,0 +1,130 @@
1
+ import type { ChatMessage } from '../../types/sdk-messages';
2
+ import { useThemeStore } from '../../themes';
3
+ import ReactMarkdown from 'react-markdown';
4
+ import remarkGfm from 'remark-gfm';
5
+
6
+ interface Props {
7
+ messages: ChatMessage[];
8
+ streamingText: string;
9
+ }
10
+
11
+ export function MessageList({ messages, streamingText }: Props) {
12
+ const theme = useThemeStore((s) => s.activeTheme);
13
+
14
+ return (
15
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
16
+ {messages.map((msg) => (
17
+ <MessageBubble key={msg.id} message={msg} />
18
+ ))}
19
+
20
+ {/* Streaming text */}
21
+ {streamingText && (
22
+ <div className="animate-fade-in" style={{
23
+ padding: '8px 16px', borderLeft: `2px solid ${theme.colors.accent}`,
24
+ margin: '0 16px',
25
+ }}>
26
+ <div className="markdown-content" style={{ color: theme.colors.text, fontSize: 13 }}>
27
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{streamingText}</ReactMarkdown>
28
+ </div>
29
+ <span className="animate-pulse" style={{
30
+ color: theme.colors.accent, fontSize: 16, marginLeft: 2,
31
+ }}>▌</span>
32
+ </div>
33
+ )}
34
+ </div>
35
+ );
36
+ }
37
+
38
+ function MessageBubble({ message }: { message: ChatMessage }) {
39
+ const theme = useThemeStore((s) => s.activeTheme);
40
+
41
+ const styles: Record<string, React.CSSProperties> = {
42
+ user: {
43
+ background: theme.colors.bgPanel,
44
+ borderLeft: `2px solid ${theme.colors.accent}`,
45
+ color: theme.colors.text,
46
+ },
47
+ assistant: {
48
+ background: 'transparent',
49
+ borderLeft: `2px solid ${theme.colors.model}`,
50
+ color: theme.colors.text,
51
+ },
52
+ tool_call: {
53
+ background: theme.colors.bgSurface,
54
+ borderLeft: `2px solid ${theme.colors.tokens}`,
55
+ color: theme.colors.text,
56
+ },
57
+ tool_result: {
58
+ background: theme.colors.bgSurface,
59
+ borderLeft: `2px solid ${message.isError ? theme.colors.error : theme.colors.success}`,
60
+ color: theme.colors.text,
61
+ },
62
+ system: {
63
+ background: 'transparent',
64
+ borderLeft: `2px solid ${message.isError ? theme.colors.error : theme.colors.textDim}`,
65
+ color: message.isError ? theme.colors.error : theme.colors.textMuted,
66
+ },
67
+ };
68
+
69
+ const roleLabels: Record<string, string> = {
70
+ user: 'You',
71
+ assistant: 'Claude',
72
+ tool_call: message.toolName || 'Tool',
73
+ tool_result: `${message.toolName || 'Result'}`,
74
+ system: 'System',
75
+ };
76
+
77
+ const roleColors: Record<string, string> = {
78
+ user: theme.colors.accent,
79
+ assistant: theme.colors.model,
80
+ tool_call: theme.colors.tokens,
81
+ tool_result: message.isError ? theme.colors.error : theme.colors.success,
82
+ system: theme.colors.textDim,
83
+ };
84
+
85
+ const isToolResult = message.role === 'tool_result';
86
+ const isToolCall = message.role === 'tool_call';
87
+ const isCollapsible = (isToolResult || isToolCall) && message.content.length > 200;
88
+
89
+ return (
90
+ <div className="animate-fade-in" style={{
91
+ padding: '8px 16px', margin: '0 16px', borderRadius: 4,
92
+ ...styles[message.role],
93
+ }}>
94
+ {/* Role label */}
95
+ <div style={{
96
+ fontSize: 11, fontWeight: 600, marginBottom: 4,
97
+ color: roleColors[message.role],
98
+ display: 'flex', alignItems: 'center', gap: 6,
99
+ }}>
100
+ <span>{roleLabels[message.role]}</span>
101
+ {isToolCall && (
102
+ <span style={{
103
+ fontSize: 10, padding: '1px 6px', borderRadius: 3,
104
+ background: theme.colors.bgHover, color: theme.colors.tokens,
105
+ }}>
106
+ {message.toolName}
107
+ </span>
108
+ )}
109
+ </div>
110
+
111
+ {/* Content */}
112
+ {message.role === 'assistant' || message.role === 'user' ? (
113
+ <div className="markdown-content" style={{ fontSize: 13 }}>
114
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
115
+ </div>
116
+ ) : (
117
+ <pre style={{
118
+ fontSize: 12, fontFamily: 'var(--font-mono)',
119
+ whiteSpace: 'pre-wrap', wordBreak: 'break-word',
120
+ maxHeight: isCollapsible ? 150 : undefined,
121
+ overflow: isCollapsible ? 'hidden' : undefined,
122
+ color: theme.colors.text, background: 'transparent',
123
+ padding: 0, margin: 0,
124
+ }}>
125
+ {message.content}
126
+ </pre>
127
+ )}
128
+ </div>
129
+ );
130
+ }
@@ -0,0 +1,70 @@
1
+ import { usePermissionStore } from '../../store/permission-store';
2
+ import { useThemeStore } from '../../themes';
3
+ import { respondPermission } from '../../services/stream-consumer';
4
+
5
+ export function PermissionDialog() {
6
+ const theme = useThemeStore((s) => s.activeTheme);
7
+ const pending = usePermissionStore((s) => s.pending);
8
+
9
+ if (pending.length === 0) return null;
10
+
11
+ const request = pending[0];
12
+
13
+ // Risk level by tool name
14
+ const highRisk = ['Bash', 'Write', 'Edit', 'MultiEdit', 'NotebookEdit'];
15
+ const isHigh = highRisk.includes(request.toolName);
16
+
17
+ return (
18
+ <div className="animate-slide-up" style={{
19
+ position: 'absolute', bottom: 80, left: 16, right: 16,
20
+ background: theme.colors.bgSurface, borderRadius: 8,
21
+ border: `1px solid ${isHigh ? theme.colors.warning : theme.colors.border}`,
22
+ padding: 16, zIndex: 100,
23
+ boxShadow: `0 4px 24px rgba(0,0,0,0.4)`,
24
+ }}>
25
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
26
+ <span style={{
27
+ fontSize: 10, fontWeight: 600, padding: '2px 6px', borderRadius: 3,
28
+ background: isHigh ? theme.colors.warning : theme.colors.success,
29
+ color: theme.colors.bg,
30
+ }}>
31
+ {isHigh ? 'HIGH RISK' : 'LOW RISK'}
32
+ </span>
33
+ <span style={{ color: theme.colors.text, fontWeight: 600, fontSize: 14 }}>
34
+ {request.toolName}
35
+ </span>
36
+ {pending.length > 1 && (
37
+ <span style={{ color: theme.colors.textDim, fontSize: 11 }}>
38
+ +{pending.length - 1} more
39
+ </span>
40
+ )}
41
+ </div>
42
+
43
+ <pre style={{
44
+ fontSize: 11, fontFamily: 'var(--font-mono)', color: theme.colors.textMuted,
45
+ background: theme.colors.bgPanel, borderRadius: 4, padding: 8,
46
+ maxHeight: 120, overflow: 'auto', whiteSpace: 'pre-wrap',
47
+ marginBottom: 12,
48
+ }}>
49
+ {JSON.stringify(request.toolInput, null, 2)}
50
+ </pre>
51
+
52
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
53
+ <button onClick={() => respondPermission(request.requestId, false)} style={{
54
+ background: theme.colors.bgPanel, color: theme.colors.error,
55
+ border: `1px solid ${theme.colors.error}`, borderRadius: 6,
56
+ padding: '6px 16px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
57
+ }}>
58
+ Deny
59
+ </button>
60
+ <button onClick={() => respondPermission(request.requestId, true)} style={{
61
+ background: theme.colors.success, color: '#fff',
62
+ border: 'none', borderRadius: 6,
63
+ padding: '6px 16px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
64
+ }}>
65
+ Allow
66
+ </button>
67
+ </div>
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,152 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useThemeStore } from '../../themes';
3
+
4
+ interface Project {
5
+ path: string;
6
+ name: string;
7
+ lastUsed: number;
8
+ }
9
+
10
+ interface Props {
11
+ currentCwd: string;
12
+ onSelect: (path: string) => void;
13
+ isOpen: boolean;
14
+ onClose: () => void;
15
+ }
16
+
17
+ export function FolderSelector({ currentCwd, onSelect, isOpen, onClose }: Props) {
18
+ const theme = useThemeStore((s) => s.activeTheme);
19
+ const [projects, setProjects] = useState<Project[]>([]);
20
+ const [customPath, setCustomPath] = useState('');
21
+ const [loading, setLoading] = useState(false);
22
+
23
+ useEffect(() => {
24
+ if (isOpen) {
25
+ loadProjects();
26
+ }
27
+ }, [isOpen]);
28
+
29
+ async function loadProjects() {
30
+ setLoading(true);
31
+ try {
32
+ const res = await fetch('/api/projects');
33
+ if (res.ok) {
34
+ const data = await res.json();
35
+ setProjects(data.projects || []);
36
+ }
37
+ } catch {
38
+ // Server might not have this endpoint yet
39
+ }
40
+ setLoading(false);
41
+ }
42
+
43
+ if (!isOpen) return null;
44
+
45
+ const c = theme.colors;
46
+
47
+ return (
48
+ <div style={{
49
+ position: 'fixed', inset: 0, zIndex: 1000,
50
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
51
+ background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(4px)',
52
+ }} onClick={onClose}>
53
+ <div style={{
54
+ background: c.bgSurface, border: `1px solid ${c.border}`,
55
+ borderRadius: 12, padding: 24, width: 480, maxHeight: '70vh',
56
+ display: 'flex', flexDirection: 'column', gap: 16,
57
+ }} onClick={(e) => e.stopPropagation()}>
58
+ {/* Header */}
59
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
60
+ <div>
61
+ <h2 style={{ color: c.text, fontSize: 16, fontWeight: 700, margin: 0 }}>
62
+ Open Project
63
+ </h2>
64
+ <p style={{ color: c.textMuted, fontSize: 12, margin: '4px 0 0' }}>
65
+ Select a folder to start a Claude Code session
66
+ </p>
67
+ </div>
68
+ <button onClick={onClose} style={{
69
+ background: 'none', border: 'none', color: c.textMuted,
70
+ fontSize: 20, cursor: 'pointer', padding: 4,
71
+ }}>×</button>
72
+ </div>
73
+
74
+ {/* Custom path input */}
75
+ <div style={{ display: 'flex', gap: 8 }}>
76
+ <input
77
+ type="text"
78
+ value={customPath}
79
+ onChange={(e) => setCustomPath(e.target.value)}
80
+ placeholder="Enter folder path..."
81
+ style={{
82
+ flex: 1, background: c.bgPanel, color: c.text,
83
+ border: `1px solid ${c.border}`, borderRadius: 6,
84
+ padding: '8px 12px', fontSize: 13, outline: 'none',
85
+ fontFamily: 'var(--font-mono)',
86
+ }}
87
+ onKeyDown={(e) => {
88
+ if (e.key === 'Enter' && customPath.trim()) {
89
+ onSelect(customPath.trim());
90
+ onClose();
91
+ }
92
+ }}
93
+ />
94
+ <button onClick={() => {
95
+ if (customPath.trim()) {
96
+ onSelect(customPath.trim());
97
+ onClose();
98
+ }
99
+ }} style={{
100
+ background: c.accent, color: '#fff', border: 'none',
101
+ borderRadius: 6, padding: '8px 16px', fontSize: 12,
102
+ fontWeight: 600, cursor: 'pointer', flexShrink: 0,
103
+ }}>
104
+ Open
105
+ </button>
106
+ </div>
107
+
108
+ {/* Current project */}
109
+ {currentCwd && (
110
+ <div style={{
111
+ background: c.bgPanel, borderRadius: 6, padding: '8px 12px',
112
+ border: `1px solid ${c.accent}30`,
113
+ }}>
114
+ <div style={{ fontSize: 10, color: c.accent, fontWeight: 600, marginBottom: 2 }}>
115
+ CURRENT
116
+ </div>
117
+ <div style={{ fontSize: 12, color: c.text, fontFamily: 'var(--font-mono)' }}>
118
+ {currentCwd}
119
+ </div>
120
+ </div>
121
+ )}
122
+
123
+ {/* Discovered projects */}
124
+ {projects.length > 0 && (
125
+ <div style={{ flex: 1, overflowY: 'auto' }}>
126
+ <div style={{ fontSize: 10, color: c.textDim, fontWeight: 600, marginBottom: 8 }}>
127
+ RECENT PROJECTS
128
+ </div>
129
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
130
+ {projects.map((p) => (
131
+ <button key={p.path} onClick={() => { onSelect(p.path); onClose(); }} style={{
132
+ background: c.bgPanel, border: `1px solid ${c.border}`,
133
+ borderRadius: 6, padding: '8px 12px', cursor: 'pointer',
134
+ textAlign: 'left', display: 'flex', flexDirection: 'column', gap: 2,
135
+ }}>
136
+ <span style={{ color: c.text, fontSize: 13, fontWeight: 500 }}>{p.name}</span>
137
+ <span style={{ color: c.textDim, fontSize: 11, fontFamily: 'var(--font-mono)' }}>{p.path}</span>
138
+ </button>
139
+ ))}
140
+ </div>
141
+ </div>
142
+ )}
143
+
144
+ {loading && (
145
+ <div style={{ color: c.textMuted, fontSize: 12, textAlign: 'center', padding: 16 }}>
146
+ Scanning for projects...
147
+ </div>
148
+ )}
149
+ </div>
150
+ </div>
151
+ );
152
+ }
@@ -0,0 +1,65 @@
1
+ import { useState } from 'react';
2
+ import { useSessionStore } from '../../store/session-store';
3
+ import { useThemeStore } from '../../themes';
4
+
5
+ const MODELS = [
6
+ { id: 'claude-opus-4-6', name: 'Opus 4.6', tier: 'max' },
7
+ { id: 'claude-sonnet-4-6', name: 'Sonnet 4.6', tier: 'balanced' },
8
+ { id: 'claude-sonnet-4-20250514', name: 'Sonnet 4', tier: 'balanced' },
9
+ { id: 'claude-haiku-4-5-20251001', name: 'Haiku 4.5', tier: 'fast' },
10
+ ];
11
+
12
+ const TIER_COLORS: Record<string, string> = {
13
+ max: '#a855f7',
14
+ balanced: '#60a5fa',
15
+ fast: '#22c55e',
16
+ };
17
+
18
+ interface Props {
19
+ isOpen: boolean;
20
+ onClose: () => void;
21
+ onSelect: (modelId: string) => void;
22
+ }
23
+
24
+ export function ModelSelector({ isOpen, onClose, onSelect }: Props) {
25
+ const theme = useThemeStore((s) => s.activeTheme);
26
+ const currentModel = useSessionStore((s) => s.model);
27
+ const c = theme.colors;
28
+
29
+ if (!isOpen) return null;
30
+
31
+ return (
32
+ <div style={{
33
+ position: 'absolute', top: '100%', right: 0, marginTop: 4,
34
+ background: c.bgSurface, border: `1px solid ${c.border}`,
35
+ borderRadius: 8, padding: 4, width: 220, zIndex: 100,
36
+ boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
37
+ }}>
38
+ <div style={{ padding: '4px 8px', fontSize: 10, color: c.textDim, fontWeight: 600 }}>
39
+ SELECT MODEL
40
+ </div>
41
+ {MODELS.map((m) => {
42
+ const isActive = currentModel.toLowerCase().includes(m.name.toLowerCase().split(' ')[0].toLowerCase());
43
+ return (
44
+ <button key={m.id} onClick={() => { onSelect(m.id); onClose(); }} style={{
45
+ width: '100%', background: isActive ? `${c.accent}15` : 'transparent',
46
+ border: isActive ? `1px solid ${c.accent}30` : '1px solid transparent',
47
+ borderRadius: 6, padding: '8px 10px', cursor: 'pointer',
48
+ display: 'flex', alignItems: 'center', gap: 8, textAlign: 'left',
49
+ }}>
50
+ <span style={{
51
+ width: 6, height: 6, borderRadius: '50%',
52
+ background: TIER_COLORS[m.tier],
53
+ flexShrink: 0,
54
+ }} />
55
+ <div style={{ flex: 1 }}>
56
+ <div style={{ color: c.text, fontSize: 13, fontWeight: 500 }}>{m.name}</div>
57
+ <div style={{ color: c.textDim, fontSize: 10 }}>{m.tier}</div>
58
+ </div>
59
+ {isActive && <span style={{ color: c.accent, fontSize: 12 }}>●</span>}
60
+ </button>
61
+ );
62
+ })}
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,115 @@
1
+ import { useThemeStore } from '../../themes';
2
+ import { useSessionStore } from '../../store/session-store';
3
+ import { useStatuslineStore } from '../../store/statusline-store';
4
+
5
+ interface Props {
6
+ isOpen: boolean;
7
+ onClose: () => void;
8
+ onResume: (sessionId: string) => void;
9
+ }
10
+
11
+ export function SessionManager({ isOpen, onClose, onResume }: Props) {
12
+ const theme = useThemeStore((s) => s.activeTheme);
13
+ const sessionId = useSessionStore((s) => s.sessionId);
14
+ const model = useStatuslineStore((s) => s.model);
15
+ const cost = useStatuslineStore((s) => s.costFormatted);
16
+ const turns = useStatuslineStore((s) => s.numTurns);
17
+ const duration = useStatuslineStore((s) => s.durationFormatted);
18
+ const c = theme.colors;
19
+
20
+ if (!isOpen) return null;
21
+
22
+ return (
23
+ <div style={{
24
+ position: 'fixed', inset: 0, zIndex: 1000,
25
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
26
+ background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(4px)',
27
+ }} onClick={onClose}>
28
+ <div style={{
29
+ background: c.bgSurface, border: `1px solid ${c.border}`,
30
+ borderRadius: 12, padding: 24, width: 420,
31
+ display: 'flex', flexDirection: 'column', gap: 16,
32
+ }} onClick={(e) => e.stopPropagation()}>
33
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
34
+ <h2 style={{ color: c.text, fontSize: 16, fontWeight: 700, margin: 0 }}>
35
+ Session Info
36
+ </h2>
37
+ <button onClick={onClose} style={{
38
+ background: 'none', border: 'none', color: c.textMuted,
39
+ fontSize: 20, cursor: 'pointer',
40
+ }}>×</button>
41
+ </div>
42
+
43
+ {sessionId ? (
44
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
45
+ <InfoRow label="Session ID" value={sessionId} color={c.accent} mono />
46
+ <InfoRow label="Model" value={model} color={c.model} />
47
+ <InfoRow label="Cost" value={cost} color={c.cost} />
48
+ <InfoRow label="Turns" value={String(turns)} color={c.text} />
49
+ <InfoRow label="Duration" value={duration} color={c.textMuted} />
50
+
51
+ <div style={{ marginTop: 8, display: 'flex', gap: 8 }}>
52
+ <button onClick={() => {
53
+ navigator.clipboard.writeText(sessionId);
54
+ }} style={{
55
+ flex: 1, background: c.bgPanel, color: c.text,
56
+ border: `1px solid ${c.border}`, borderRadius: 6,
57
+ padding: '8px 12px', fontSize: 12, cursor: 'pointer',
58
+ }}>
59
+ Copy Session ID
60
+ </button>
61
+ </div>
62
+ </div>
63
+ ) : (
64
+ <div style={{ color: c.textMuted, fontSize: 13, textAlign: 'center', padding: 16 }}>
65
+ No active session. Send a message to start one.
66
+ </div>
67
+ )}
68
+
69
+ {/* Resume section */}
70
+ <div style={{ borderTop: `1px solid ${c.border}`, paddingTop: 16 }}>
71
+ <div style={{ fontSize: 10, color: c.textDim, fontWeight: 600, marginBottom: 8 }}>
72
+ RESUME SESSION
73
+ </div>
74
+ <div style={{ display: 'flex', gap: 8 }}>
75
+ <input
76
+ type="text"
77
+ placeholder="Paste session ID..."
78
+ style={{
79
+ flex: 1, background: c.bgPanel, color: c.text,
80
+ border: `1px solid ${c.border}`, borderRadius: 6,
81
+ padding: '8px 12px', fontSize: 12, outline: 'none',
82
+ fontFamily: 'var(--font-mono)',
83
+ }}
84
+ onKeyDown={(e) => {
85
+ if (e.key === 'Enter') {
86
+ const val = (e.target as HTMLInputElement).value.trim();
87
+ if (val) { onResume(val); onClose(); }
88
+ }
89
+ }}
90
+ />
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ function InfoRow({ label, value, color, mono }: {
99
+ label: string; value: string; color: string; mono?: boolean;
100
+ }) {
101
+ const theme = useThemeStore((s) => s.activeTheme);
102
+ return (
103
+ <div style={{
104
+ display: 'flex', justifyContent: 'space-between', alignItems: 'center',
105
+ padding: '6px 10px', background: theme.colors.bgPanel, borderRadius: 4,
106
+ }}>
107
+ <span style={{ color: theme.colors.textMuted, fontSize: 12 }}>{label}</span>
108
+ <span style={{
109
+ color, fontSize: 12, fontWeight: 500,
110
+ fontFamily: mono ? 'var(--font-mono)' : 'inherit',
111
+ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis',
112
+ }}>{value}</span>
113
+ </div>
114
+ );
115
+ }