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,93 @@
1
+ import {
2
+ BaseBoxShapeUtil,
3
+ HTMLContainer,
4
+ type TLBaseShape,
5
+ } from '@tldraw/tldraw';
6
+ import { SHAPE_DEFAULTS, NODE_COLORS } from './shared-styles';
7
+
8
+ export type SessionNodeShape = TLBaseShape<
9
+ 'sessionNode',
10
+ {
11
+ w: number;
12
+ h: number;
13
+ model: string;
14
+ cwd: string;
15
+ sessionId: string;
16
+ toolCount: number;
17
+ }
18
+ >;
19
+
20
+ // @ts-expect-error — custom shape type not in tldraw's built-in TLShape union
21
+ export class SessionNodeShapeUtil extends BaseBoxShapeUtil<SessionNodeShape> {
22
+ static type = 'sessionNode' as const;
23
+
24
+ getDefaultProps(): SessionNodeShape['props'] {
25
+ return {
26
+ w: 280,
27
+ h: 84,
28
+ model: 'unknown',
29
+ cwd: '',
30
+ sessionId: '',
31
+ toolCount: 0,
32
+ };
33
+ }
34
+
35
+ component(shape: SessionNodeShape) {
36
+ const color = NODE_COLORS.session;
37
+ return (
38
+ <HTMLContainer>
39
+ <div
40
+ style={{
41
+ width: shape.props.w,
42
+ height: shape.props.h,
43
+ background: '#1a1a2e',
44
+ border: `2px solid ${color}`,
45
+ borderRadius: SHAPE_DEFAULTS.borderRadius,
46
+ padding: SHAPE_DEFAULTS.padding,
47
+ fontFamily: 'Inter, system-ui, sans-serif',
48
+ display: 'flex',
49
+ flexDirection: 'column',
50
+ gap: 4,
51
+ pointerEvents: 'all',
52
+ }}
53
+ >
54
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
55
+ <span
56
+ style={{
57
+ background: color,
58
+ color: '#0a0a0f',
59
+ fontSize: 9,
60
+ fontWeight: 800,
61
+ padding: '2px 6px',
62
+ borderRadius: 3,
63
+ letterSpacing: 1,
64
+ }}
65
+ >
66
+ SESSION
67
+ </span>
68
+ <span style={{ color, fontSize: 13, fontWeight: 700 }}>
69
+ {shape.props.model}
70
+ </span>
71
+ </div>
72
+ <div style={{ color: '#787882', fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
73
+ {shape.props.cwd}
74
+ </div>
75
+ <div style={{ color: '#505058', fontSize: 10 }}>
76
+ {shape.props.toolCount} tools available
77
+ </div>
78
+ </div>
79
+ </HTMLContainer>
80
+ );
81
+ }
82
+
83
+ indicator(shape: SessionNodeShape) {
84
+ return (
85
+ <rect
86
+ width={shape.props.w}
87
+ height={shape.props.h}
88
+ rx={SHAPE_DEFAULTS.borderRadius}
89
+ ry={SHAPE_DEFAULTS.borderRadius}
90
+ />
91
+ );
92
+ }
93
+ }
@@ -0,0 +1,125 @@
1
+ import {
2
+ BaseBoxShapeUtil,
3
+ HTMLContainer,
4
+ type TLBaseShape,
5
+ } from '@tldraw/tldraw';
6
+ import { NODE_COLORS } from './shared-styles';
7
+
8
+ export type StatuslineWidgetShape = TLBaseShape<
9
+ 'statuslineWidget',
10
+ {
11
+ w: number;
12
+ h: number;
13
+ model: string;
14
+ cost: string;
15
+ tokensIn: string;
16
+ tokensOut: string;
17
+ contextPct: number;
18
+ skill: string;
19
+ turns: number;
20
+ }
21
+ >;
22
+
23
+ // @ts-expect-error — custom shape type not in tldraw's built-in TLShape union
24
+ export class StatuslineWidgetShapeUtil extends BaseBoxShapeUtil<StatuslineWidgetShape> {
25
+ static type = 'statuslineWidget' as const;
26
+
27
+ getDefaultProps(): StatuslineWidgetShape['props'] {
28
+ return {
29
+ w: 220,
30
+ h: 140,
31
+ model: 'unknown',
32
+ cost: '$0.00',
33
+ tokensIn: '0',
34
+ tokensOut: '0',
35
+ contextPct: 0,
36
+ skill: 'Idle',
37
+ turns: 0,
38
+ };
39
+ }
40
+
41
+ component(shape: StatuslineWidgetShape) {
42
+ const { model, cost, tokensIn, tokensOut, contextPct, skill, turns } = shape.props;
43
+ const color = NODE_COLORS.statusline;
44
+
45
+ const ctxColor = contextPct > 90 ? '#dc2626'
46
+ : contextPct > 75 ? '#ef4444'
47
+ : contextPct > 40 ? '#fb923c'
48
+ : '#e4e4e7';
49
+
50
+ return (
51
+ <HTMLContainer>
52
+ <div
53
+ style={{
54
+ width: shape.props.w,
55
+ height: shape.props.h,
56
+ background: '#0a0a0f',
57
+ border: `1.5px solid ${color}50`,
58
+ borderRadius: 10,
59
+ padding: 10,
60
+ fontFamily: 'JetBrains Mono, monospace',
61
+ fontSize: 10,
62
+ display: 'flex',
63
+ flexDirection: 'column',
64
+ gap: 3,
65
+ pointerEvents: 'all',
66
+ }}
67
+ >
68
+ {/* Header */}
69
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 2 }}>
70
+ <span style={{
71
+ background: color, color: '#0a0a0f', fontSize: 8,
72
+ fontWeight: 800, padding: '1px 4px', borderRadius: 2,
73
+ letterSpacing: 1,
74
+ }}>UC</span>
75
+ <span style={{ color, fontSize: 10, fontWeight: 600 }}>Statusline</span>
76
+ </div>
77
+
78
+ {/* Fields */}
79
+ <Row label="Model" value={model} color="#a855f7" />
80
+ <Row label="Skill" value={skill} color={color} />
81
+ <Row label="Tokens" value={`${tokensIn} + ${tokensOut}`} color="#f59e0b" />
82
+ <Row label="Cost" value={cost} color="#22c55e" />
83
+ <Row label="Turns" value={String(turns)} color="#787882" />
84
+
85
+ {/* Context bar */}
86
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 2 }}>
87
+ <span style={{ color: ctxColor, minWidth: 28 }}>Ctx</span>
88
+ <div style={{
89
+ flex: 1, height: 5, borderRadius: 3,
90
+ background: '#28282d', overflow: 'hidden',
91
+ }}>
92
+ <div style={{
93
+ width: `${contextPct}%`, height: '100%',
94
+ background: ctxColor, borderRadius: 3,
95
+ }} />
96
+ </div>
97
+ <span style={{ color: ctxColor, minWidth: 26, textAlign: 'right' }}>
98
+ {contextPct}%
99
+ </span>
100
+ </div>
101
+ </div>
102
+ </HTMLContainer>
103
+ );
104
+ }
105
+
106
+ indicator(shape: StatuslineWidgetShape) {
107
+ return (
108
+ <rect
109
+ width={shape.props.w}
110
+ height={shape.props.h}
111
+ rx={10}
112
+ ry={10}
113
+ />
114
+ );
115
+ }
116
+ }
117
+
118
+ function Row({ label, value, color }: { label: string; value: string; color: string }) {
119
+ return (
120
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
121
+ <span style={{ color: '#505058' }}>{label}</span>
122
+ <span style={{ color, fontWeight: 500 }}>{value}</span>
123
+ </div>
124
+ );
125
+ }
@@ -0,0 +1,86 @@
1
+ import {
2
+ BaseBoxShapeUtil,
3
+ HTMLContainer,
4
+ type TLBaseShape,
5
+ } from '@tldraw/tldraw';
6
+ import { SHAPE_DEFAULTS, NODE_COLORS, truncate } from './shared-styles';
7
+
8
+ export type TextResponseNodeShape = TLBaseShape<
9
+ 'textResponseNode',
10
+ {
11
+ w: number;
12
+ h: number;
13
+ text: string;
14
+ }
15
+ >;
16
+
17
+ // @ts-expect-error — custom shape type not in tldraw's built-in TLShape union
18
+ export class TextResponseNodeShapeUtil extends BaseBoxShapeUtil<TextResponseNodeShape> {
19
+ static type = 'textResponseNode' as const;
20
+
21
+ getDefaultProps(): TextResponseNodeShape['props'] {
22
+ return {
23
+ w: 280,
24
+ h: 72,
25
+ text: '',
26
+ };
27
+ }
28
+
29
+ component(shape: TextResponseNodeShape) {
30
+ const color = NODE_COLORS.textResponse;
31
+
32
+ return (
33
+ <HTMLContainer>
34
+ <div
35
+ style={{
36
+ width: shape.props.w,
37
+ height: shape.props.h,
38
+ background: '#141419',
39
+ border: `1.5px solid ${color}30`,
40
+ borderLeft: `3px solid ${color}`,
41
+ borderRadius: SHAPE_DEFAULTS.borderRadius,
42
+ padding: SHAPE_DEFAULTS.padding,
43
+ fontFamily: 'Inter, system-ui, sans-serif',
44
+ display: 'flex',
45
+ flexDirection: 'column',
46
+ gap: 4,
47
+ overflow: 'hidden',
48
+ pointerEvents: 'all',
49
+ }}
50
+ >
51
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
52
+ <span style={{ fontSize: 12 }}>💬</span>
53
+ <span style={{ color, fontSize: 11, fontWeight: 600 }}>
54
+ Claude Response
55
+ </span>
56
+ </div>
57
+ <div
58
+ style={{
59
+ color: '#a0a0a8',
60
+ fontSize: 11,
61
+ lineHeight: 1.4,
62
+ overflow: 'hidden',
63
+ textOverflow: 'ellipsis',
64
+ display: '-webkit-box',
65
+ WebkitLineClamp: 2,
66
+ WebkitBoxOrient: 'vertical',
67
+ }}
68
+ >
69
+ {truncate(shape.props.text, 200)}
70
+ </div>
71
+ </div>
72
+ </HTMLContainer>
73
+ );
74
+ }
75
+
76
+ indicator(shape: TextResponseNodeShape) {
77
+ return (
78
+ <rect
79
+ width={shape.props.w}
80
+ height={shape.props.h}
81
+ rx={SHAPE_DEFAULTS.borderRadius}
82
+ ry={SHAPE_DEFAULTS.borderRadius}
83
+ />
84
+ );
85
+ }
86
+ }
@@ -0,0 +1,107 @@
1
+ import {
2
+ BaseBoxShapeUtil,
3
+ HTMLContainer,
4
+ type TLBaseShape,
5
+ } from '@tldraw/tldraw';
6
+ import { SHAPE_DEFAULTS, NODE_COLORS, getToolIcon, truncate } from './shared-styles';
7
+
8
+ export type ToolCallNodeShape = TLBaseShape<
9
+ 'toolCallNode',
10
+ {
11
+ w: number;
12
+ h: number;
13
+ toolName: string;
14
+ toolInput: string;
15
+ status: 'pending' | 'running' | 'complete' | 'denied';
16
+ }
17
+ >;
18
+
19
+ const STATUS_COLORS: Record<string, string> = {
20
+ pending: '#f59e0b',
21
+ running: '#60a5fa',
22
+ complete: '#22c55e',
23
+ denied: '#ef4444',
24
+ };
25
+
26
+ // @ts-expect-error — custom shape type not in tldraw's built-in TLShape union
27
+ export class ToolCallNodeShapeUtil extends BaseBoxShapeUtil<ToolCallNodeShape> {
28
+ static type = 'toolCallNode' as const;
29
+
30
+ getDefaultProps(): ToolCallNodeShape['props'] {
31
+ return {
32
+ w: 260,
33
+ h: 72,
34
+ toolName: '',
35
+ toolInput: '',
36
+ status: 'complete',
37
+ };
38
+ }
39
+
40
+ component(shape: ToolCallNodeShape) {
41
+ const color = NODE_COLORS.toolCall;
42
+ const statusColor = STATUS_COLORS[shape.props.status];
43
+ const icon = getToolIcon(shape.props.toolName);
44
+
45
+ return (
46
+ <HTMLContainer>
47
+ <div
48
+ style={{
49
+ width: shape.props.w,
50
+ height: shape.props.h,
51
+ background: '#141419',
52
+ border: `1.5px solid ${color}40`,
53
+ borderLeft: `3px solid ${color}`,
54
+ borderRadius: SHAPE_DEFAULTS.borderRadius,
55
+ padding: SHAPE_DEFAULTS.padding,
56
+ fontFamily: 'Inter, system-ui, sans-serif',
57
+ display: 'flex',
58
+ flexDirection: 'column',
59
+ gap: 4,
60
+ pointerEvents: 'all',
61
+ }}
62
+ >
63
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
64
+ <span style={{ fontSize: 14 }}>{icon}</span>
65
+ <span style={{ color, fontSize: 12, fontWeight: 600 }}>
66
+ {shape.props.toolName}
67
+ </span>
68
+ <span
69
+ style={{
70
+ marginLeft: 'auto',
71
+ fontSize: 9,
72
+ color: statusColor,
73
+ fontWeight: 600,
74
+ textTransform: 'uppercase',
75
+ }}
76
+ >
77
+ {shape.props.status}
78
+ </span>
79
+ </div>
80
+ <div
81
+ style={{
82
+ color: '#787882',
83
+ fontSize: 10,
84
+ fontFamily: 'JetBrains Mono, monospace',
85
+ overflow: 'hidden',
86
+ textOverflow: 'ellipsis',
87
+ whiteSpace: 'nowrap',
88
+ }}
89
+ >
90
+ {truncate(shape.props.toolInput, 80)}
91
+ </div>
92
+ </div>
93
+ </HTMLContainer>
94
+ );
95
+ }
96
+
97
+ indicator(shape: ToolCallNodeShape) {
98
+ return (
99
+ <rect
100
+ width={shape.props.w}
101
+ height={shape.props.h}
102
+ rx={SHAPE_DEFAULTS.borderRadius}
103
+ ry={SHAPE_DEFAULTS.borderRadius}
104
+ />
105
+ );
106
+ }
107
+ }
@@ -0,0 +1,87 @@
1
+ import {
2
+ BaseBoxShapeUtil,
3
+ HTMLContainer,
4
+ type TLBaseShape,
5
+ } from '@tldraw/tldraw';
6
+ import { SHAPE_DEFAULTS, NODE_COLORS, truncate } from './shared-styles';
7
+
8
+ export type ToolResultNodeShape = TLBaseShape<
9
+ 'toolResultNode',
10
+ {
11
+ w: number;
12
+ h: number;
13
+ toolName: string;
14
+ outputSummary: string;
15
+ isError: boolean;
16
+ }
17
+ >;
18
+
19
+ // @ts-expect-error — custom shape type not in tldraw's built-in TLShape union
20
+ export class ToolResultNodeShapeUtil extends BaseBoxShapeUtil<ToolResultNodeShape> {
21
+ static type = 'toolResultNode' as const;
22
+
23
+ getDefaultProps(): ToolResultNodeShape['props'] {
24
+ return {
25
+ w: 260,
26
+ h: 64,
27
+ toolName: '',
28
+ outputSummary: '',
29
+ isError: false,
30
+ };
31
+ }
32
+
33
+ component(shape: ToolResultNodeShape) {
34
+ const color = shape.props.isError ? NODE_COLORS.compaction : NODE_COLORS.toolResult;
35
+
36
+ return (
37
+ <HTMLContainer>
38
+ <div
39
+ style={{
40
+ width: shape.props.w,
41
+ height: shape.props.h,
42
+ background: '#141419',
43
+ border: `1.5px solid ${color}30`,
44
+ borderLeft: `3px solid ${color}`,
45
+ borderRadius: SHAPE_DEFAULTS.borderRadius,
46
+ padding: SHAPE_DEFAULTS.padding,
47
+ fontFamily: 'Inter, system-ui, sans-serif',
48
+ display: 'flex',
49
+ flexDirection: 'column',
50
+ gap: 4,
51
+ pointerEvents: 'all',
52
+ }}
53
+ >
54
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
55
+ <span style={{ fontSize: 12 }}>{shape.props.isError ? '❌' : '✅'}</span>
56
+ <span style={{ color, fontSize: 11, fontWeight: 600 }}>
57
+ {shape.props.toolName} result
58
+ </span>
59
+ </div>
60
+ <div
61
+ style={{
62
+ color: '#787882',
63
+ fontSize: 10,
64
+ fontFamily: 'JetBrains Mono, monospace',
65
+ overflow: 'hidden',
66
+ textOverflow: 'ellipsis',
67
+ whiteSpace: 'nowrap',
68
+ }}
69
+ >
70
+ {truncate(shape.props.outputSummary, 100)}
71
+ </div>
72
+ </div>
73
+ </HTMLContainer>
74
+ );
75
+ }
76
+
77
+ indicator(shape: ToolResultNodeShape) {
78
+ return (
79
+ <rect
80
+ width={shape.props.w}
81
+ height={shape.props.h}
82
+ rx={SHAPE_DEFAULTS.borderRadius}
83
+ ry={SHAPE_DEFAULTS.borderRadius}
84
+ />
85
+ );
86
+ }
87
+ }
@@ -0,0 +1,35 @@
1
+ // Shared constants and helpers for custom tldraw shapes
2
+ export const SHAPE_DEFAULTS = {
3
+ width: 260,
4
+ height: 72,
5
+ padding: 12,
6
+ borderRadius: 8,
7
+ fontSize: 12,
8
+ labelSize: 10,
9
+ };
10
+
11
+ export const NODE_COLORS: Record<string, string> = {
12
+ session: '#a855f7',
13
+ toolCall: '#f59e0b',
14
+ toolResult: '#22c55e',
15
+ textResponse: '#60a5fa',
16
+ compaction: '#ef4444',
17
+ statusline: '#ec4899',
18
+ };
19
+
20
+ export const TOOL_ICONS: Record<string, string> = {
21
+ Read: '📄', Write: '✏️', Edit: '🔧', Bash: '⌨️', Glob: '🔍',
22
+ Grep: '🔎', Task: '🤖', WebSearch: '🌐', WebFetch: '📡',
23
+ MultiEdit: '🔧', Skill: '⚡', AskUserQuestion: '❓',
24
+ EnterPlanMode: '📋', ExitPlanMode: '✅', NotebookEdit: '📓',
25
+ default: '🔹',
26
+ };
27
+
28
+ export function getToolIcon(name: string): string {
29
+ return TOOL_ICONS[name] || TOOL_ICONS.default;
30
+ }
31
+
32
+ export function truncate(str: string, max: number): string {
33
+ if (str.length <= max) return str;
34
+ return str.slice(0, max - 1) + '…';
35
+ }
@@ -0,0 +1,96 @@
1
+ import { useRef, useEffect } from 'react';
2
+ import { useChatStore } from '../../store/chat-store';
3
+ import { useSessionStore } from '../../store/session-store';
4
+ import { useThemeStore } from '../../themes';
5
+ import { MessageList } from './MessageList';
6
+ import { InputBar } from './InputBar';
7
+ import { PermissionDialog } from './PermissionDialog';
8
+
9
+ export function ChatPanel() {
10
+ const theme = useThemeStore((s) => s.activeTheme);
11
+ const messages = useChatStore((s) => s.messages);
12
+ const streamingText = useChatStore((s) => s.streamingText);
13
+ const isConnected = useSessionStore((s) => s.isConnected);
14
+ const scrollRef = useRef<HTMLDivElement>(null);
15
+
16
+ // Auto-scroll to bottom on new messages
17
+ useEffect(() => {
18
+ if (scrollRef.current) {
19
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
20
+ }
21
+ }, [messages, streamingText]);
22
+
23
+ return (
24
+ <div style={{
25
+ flex: 1, display: 'flex', flexDirection: 'column',
26
+ minWidth: 400, position: 'relative',
27
+ }}>
28
+ {/* Connection indicator */}
29
+ <div style={{
30
+ padding: '4px 16px', fontSize: 11,
31
+ color: isConnected ? theme.colors.success : theme.colors.error,
32
+ background: theme.colors.bgSurface,
33
+ borderBottom: `1px solid ${theme.colors.border}`,
34
+ }}>
35
+ {isConnected ? '● Connected' : '○ Connecting...'}
36
+ </div>
37
+
38
+ {/* Messages */}
39
+ <div ref={scrollRef} style={{
40
+ flex: 1, overflowY: 'auto', padding: '12px 0',
41
+ }}>
42
+ {messages.length === 0 && !streamingText ? (
43
+ <WelcomeScreen />
44
+ ) : (
45
+ <MessageList messages={messages} streamingText={streamingText} />
46
+ )}
47
+ </div>
48
+
49
+ {/* Permission dialogs */}
50
+ <PermissionDialog />
51
+
52
+ {/* Input */}
53
+ <InputBar />
54
+ </div>
55
+ );
56
+ }
57
+
58
+ function WelcomeScreen() {
59
+ const theme = useThemeStore((s) => s.activeTheme);
60
+
61
+ return (
62
+ <div style={{
63
+ display: 'flex', flexDirection: 'column', alignItems: 'center',
64
+ justifyContent: 'center', height: '100%', gap: 16, padding: 40,
65
+ }}>
66
+ <div style={{
67
+ color: theme.colors.bg, fontWeight: 800, fontSize: 28,
68
+ background: theme.colors.skill, borderRadius: 8, padding: '8px 16px',
69
+ letterSpacing: 2,
70
+ }}>UC</div>
71
+ <div style={{ marginTop: 8 }}>
72
+ <span style={{ color: theme.colors.skill, fontWeight: 700, fontSize: 24 }}>UpfynAI</span>
73
+ <span style={{ color: theme.colors.accent, fontWeight: 700, fontSize: 24 }}>-Code</span>
74
+ </div>
75
+ <p style={{ color: theme.colors.textMuted, textAlign: 'center', maxWidth: 400, fontSize: 13 }}>
76
+ Visual AI coding interface powered by Claude Code SDK.
77
+ Type a message below to start a session.
78
+ </p>
79
+ <div style={{
80
+ display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'center', marginTop: 8,
81
+ }}>
82
+ {['Read the README', 'Find all TODO comments', 'Explain the architecture'].map((s) => (
83
+ <button key={s} onClick={() => {
84
+ useChatStore.getState().setInput(s);
85
+ }} style={{
86
+ background: theme.colors.bgPanel, color: theme.colors.textMuted,
87
+ border: `1px solid ${theme.colors.border}`, borderRadius: 6,
88
+ padding: '6px 12px', fontSize: 12, cursor: 'pointer',
89
+ }}>
90
+ {s}
91
+ </button>
92
+ ))}
93
+ </div>
94
+ </div>
95
+ );
96
+ }