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.
- package/LICENSE +22 -0
- package/bin/cli.js +86 -0
- package/dist/assets/CanvasPanel-B48gAKVY.js +538 -0
- package/dist/assets/CanvasPanel-B48gAKVY.js.map +1 -0
- package/dist/assets/CanvasPanel-BsOG3EVs.css +1 -0
- package/dist/assets/index-CEhTwG68.css +1 -0
- package/dist/assets/index-GqAGWpJI.js +70 -0
- package/dist/assets/index-GqAGWpJI.js.map +1 -0
- package/dist/index.html +18 -0
- package/index.html +17 -0
- package/package.json +67 -0
- package/src/App.tsx +226 -0
- package/src/components/canvas/CanvasPanel.tsx +62 -0
- package/src/components/canvas/layout/graph-builder.ts +136 -0
- package/src/components/canvas/shapes/CompactionNodeShape.tsx +76 -0
- package/src/components/canvas/shapes/SessionNodeShape.tsx +93 -0
- package/src/components/canvas/shapes/StatuslineWidgetShape.tsx +125 -0
- package/src/components/canvas/shapes/TextResponseNodeShape.tsx +86 -0
- package/src/components/canvas/shapes/ToolCallNodeShape.tsx +107 -0
- package/src/components/canvas/shapes/ToolResultNodeShape.tsx +87 -0
- package/src/components/canvas/shapes/shared-styles.ts +35 -0
- package/src/components/chat/ChatPanel.tsx +96 -0
- package/src/components/chat/InputBar.tsx +81 -0
- package/src/components/chat/MessageList.tsx +130 -0
- package/src/components/chat/PermissionDialog.tsx +70 -0
- package/src/components/layout/FolderSelector.tsx +152 -0
- package/src/components/layout/ModelSelector.tsx +65 -0
- package/src/components/layout/SessionManager.tsx +115 -0
- package/src/components/statusline/StatuslineBar.tsx +114 -0
- package/src/main.tsx +10 -0
- package/src/server/claude-session.ts +156 -0
- package/src/server/index.ts +149 -0
- package/src/services/stream-consumer.ts +330 -0
- package/src/statusline-core/bin/statusline.sh +121 -0
- package/src/statusline-core/commands/sls-config.md +42 -0
- package/src/statusline-core/commands/sls-doctor.md +35 -0
- package/src/statusline-core/commands/sls-help.md +48 -0
- package/src/statusline-core/commands/sls-layout.md +38 -0
- package/src/statusline-core/commands/sls-preview.md +34 -0
- package/src/statusline-core/commands/sls-theme.md +40 -0
- package/src/statusline-core/installer.js +228 -0
- package/src/statusline-core/layouts/compact.sh +21 -0
- package/src/statusline-core/layouts/full.sh +62 -0
- package/src/statusline-core/layouts/standard.sh +39 -0
- package/src/statusline-core/lib/core.sh +389 -0
- package/src/statusline-core/lib/helpers.sh +81 -0
- package/src/statusline-core/lib/json-parser.sh +71 -0
- package/src/statusline-core/themes/catppuccin.sh +32 -0
- package/src/statusline-core/themes/default.sh +37 -0
- package/src/statusline-core/themes/gruvbox.sh +32 -0
- package/src/statusline-core/themes/nord.sh +32 -0
- package/src/statusline-core/themes/tokyo-night.sh +32 -0
- package/src/store/canvas-store.ts +50 -0
- package/src/store/chat-store.ts +60 -0
- package/src/store/permission-store.ts +29 -0
- package/src/store/session-store.ts +52 -0
- package/src/store/statusline-store.ts +160 -0
- package/src/styles/global.css +117 -0
- package/src/themes/index.ts +149 -0
- package/src/types/canvas-graph.ts +24 -0
- package/src/types/sdk-messages.ts +156 -0
- package/src/types/statusline-fields.ts +67 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +26 -0
- 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
|
+
}
|