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,32 @@
1
+ #!/usr/bin/env bash
2
+ # Theme: Tokyo Night (vibrant neon)
3
+ THEME_NAME="Tokyo Night"
4
+
5
+ CLR_RST='\033[0m'
6
+ CLR_BOLD='\033[1m'
7
+ CLR_DIM='\033[2m'
8
+
9
+ CLR_SKILL='\033[38;2;255;117;127m' # Red/pink
10
+ CLR_MODEL='\033[38;2;187;154;247m' # Purple
11
+ CLR_DIR='\033[38;2;125;207;255m' # Cyan
12
+ CLR_GITHUB='\033[38;2;192;202;245m' # Foreground
13
+ CLR_TOKENS='\033[38;2;224;175;104m' # Yellow/orange
14
+ CLR_COST='\033[38;2;158;206;106m' # Green
15
+ CLR_VIM='\033[38;2;115;218;202m' # Teal
16
+ CLR_AGENT='\033[38;2;122;162;247m' # Blue
17
+
18
+ CLR_CTX_LOW='\033[38;2;192;202;245m' # Foreground
19
+ CLR_CTX_MED='\033[38;2;255;158;100m' # Orange
20
+ CLR_CTX_HIGH='\033[38;2;247;118;142m' # Red
21
+ CLR_CTX_CRIT='\033[38;2;219;75;75m' # Deep red
22
+
23
+ CLR_SEP='\033[38;2;59;66;97m' # Comment color
24
+ CLR_BAR_EMPTY='\033[38;2;41;46;66m' # Dark bg
25
+ CLR_LABEL='\033[38;2;86;95;137m' # Muted
26
+
27
+ CLR_GIT_STAGED='\033[38;2;158;206;106m'
28
+ CLR_GIT_UNSTAGED='\033[38;2;224;175;104m'
29
+
30
+ BAR_FILLED='█'
31
+ BAR_EMPTY='░'
32
+ SEP_CHAR='│'
@@ -0,0 +1,50 @@
1
+ import { create } from 'zustand';
2
+ import type { CanvasNode, CanvasEdge } from '../types/canvas-graph';
3
+
4
+ interface CanvasState {
5
+ nodes: CanvasNode[];
6
+ edges: CanvasEdge[];
7
+ lastNodeId: string | null;
8
+
9
+ addNode: (node: Omit<CanvasNode, 'x' | 'y'>) => void;
10
+ clear: () => void;
11
+ }
12
+
13
+ const NODE_SPACING_Y = 120;
14
+ const NODE_START_X = 100;
15
+ const NODE_START_Y = 100;
16
+
17
+ export const useCanvasStore = create<CanvasState>((set, get) => ({
18
+ nodes: [],
19
+ edges: [],
20
+ lastNodeId: null,
21
+
22
+ addNode: (partial) => {
23
+ const { nodes, edges, lastNodeId } = get();
24
+ const y = NODE_START_Y + nodes.length * NODE_SPACING_Y;
25
+ const x = NODE_START_X;
26
+
27
+ const node: CanvasNode = {
28
+ ...partial,
29
+ x,
30
+ y,
31
+ };
32
+
33
+ const newEdges = [...edges];
34
+ if (lastNodeId) {
35
+ newEdges.push({
36
+ id: `edge-${lastNodeId}-${node.id}`,
37
+ fromId: lastNodeId,
38
+ toId: node.id,
39
+ });
40
+ }
41
+
42
+ set({
43
+ nodes: [...nodes, node],
44
+ edges: newEdges,
45
+ lastNodeId: node.id,
46
+ });
47
+ },
48
+
49
+ clear: () => set({ nodes: [], edges: [], lastNodeId: null }),
50
+ }));
@@ -0,0 +1,60 @@
1
+ import { create } from 'zustand';
2
+ import type { ChatMessage, TokenUsage } from '../types/sdk-messages';
3
+
4
+ interface ChatState {
5
+ messages: ChatMessage[];
6
+ streamingText: string;
7
+ streamingToolName: string;
8
+ input: string;
9
+ isWaiting: boolean;
10
+
11
+ addMessage: (msg: ChatMessage) => void;
12
+ appendStreamDelta: (text: string) => void;
13
+ setStreamingToolName: (name: string) => void;
14
+ finalizeStreaming: (usage?: TokenUsage) => void;
15
+ setInput: (input: string) => void;
16
+ setWaiting: (waiting: boolean) => void;
17
+ clearMessages: () => void;
18
+ }
19
+
20
+ let messageCounter = 0;
21
+
22
+ export const useChatStore = create<ChatState>((set, get) => ({
23
+ messages: [],
24
+ streamingText: '',
25
+ streamingToolName: '',
26
+ input: '',
27
+ isWaiting: false,
28
+
29
+ addMessage: (msg) => set((s) => ({
30
+ messages: [...s.messages, msg],
31
+ })),
32
+
33
+ appendStreamDelta: (text) => set((s) => ({
34
+ streamingText: s.streamingText + text,
35
+ })),
36
+
37
+ setStreamingToolName: (name) => set({ streamingToolName: name }),
38
+
39
+ finalizeStreaming: (usage) => {
40
+ const { streamingText, messages } = get();
41
+ if (streamingText) {
42
+ set({
43
+ messages: [...messages, {
44
+ id: `msg-${++messageCounter}`,
45
+ role: 'assistant',
46
+ content: streamingText,
47
+ timestamp: Date.now(),
48
+ usage,
49
+ }],
50
+ streamingText: '',
51
+ streamingToolName: '',
52
+ isWaiting: false,
53
+ });
54
+ }
55
+ },
56
+
57
+ setInput: (input) => set({ input }),
58
+ setWaiting: (waiting) => set({ isWaiting: waiting }),
59
+ clearMessages: () => set({ messages: [], streamingText: '', streamingToolName: '' }),
60
+ }));
@@ -0,0 +1,29 @@
1
+ import { create } from 'zustand';
2
+
3
+ export interface PendingPermission {
4
+ requestId: string;
5
+ toolName: string;
6
+ toolInput: Record<string, unknown>;
7
+ timestamp: number;
8
+ }
9
+
10
+ interface PermissionState {
11
+ pending: PendingPermission[];
12
+ addRequest: (req: PendingPermission) => void;
13
+ removeRequest: (requestId: string) => void;
14
+ clearAll: () => void;
15
+ }
16
+
17
+ export const usePermissionStore = create<PermissionState>((set) => ({
18
+ pending: [],
19
+
20
+ addRequest: (req) => set((s) => ({
21
+ pending: [...s.pending, req],
22
+ })),
23
+
24
+ removeRequest: (requestId) => set((s) => ({
25
+ pending: s.pending.filter((p) => p.requestId !== requestId),
26
+ })),
27
+
28
+ clearAll: () => set({ pending: [] }),
29
+ }));
@@ -0,0 +1,52 @@
1
+ import { create } from 'zustand';
2
+
3
+ interface SessionState {
4
+ sessionId: string | null;
5
+ model: string;
6
+ cwd: string;
7
+ tools: string[];
8
+ permissionMode: string;
9
+ isConnected: boolean;
10
+ isStreaming: boolean;
11
+
12
+ setInit: (data: {
13
+ sessionId: string;
14
+ model: string;
15
+ cwd: string;
16
+ tools: string[];
17
+ permissionMode: string;
18
+ }) => void;
19
+ setCwd: (cwd: string) => void;
20
+ setModel: (model: string) => void;
21
+ setStreaming: (streaming: boolean) => void;
22
+ setConnected: (connected: boolean) => void;
23
+ reset: () => void;
24
+ }
25
+
26
+ export const useSessionStore = create<SessionState>((set) => ({
27
+ sessionId: null,
28
+ model: 'unknown',
29
+ cwd: '',
30
+ tools: [],
31
+ permissionMode: 'default',
32
+ isConnected: false,
33
+ isStreaming: false,
34
+
35
+ setInit: (data) => set({
36
+ sessionId: data.sessionId,
37
+ model: data.model,
38
+ cwd: data.cwd,
39
+ tools: data.tools,
40
+ permissionMode: data.permissionMode,
41
+ isConnected: true,
42
+ }),
43
+
44
+ setCwd: (cwd) => set({ cwd }),
45
+ setModel: (model) => set({ model }),
46
+ setStreaming: (streaming) => set({ isStreaming: streaming }),
47
+ setConnected: (connected) => set({ isConnected: connected }),
48
+ reset: () => set({
49
+ sessionId: null, model: 'unknown', cwd: '', tools: [],
50
+ permissionMode: 'default', isConnected: false, isStreaming: false,
51
+ }),
52
+ }));
@@ -0,0 +1,160 @@
1
+ import { create } from 'zustand';
2
+ import type { StatuslineData } from '../types/statusline-fields';
3
+ import { DEFAULT_STATUSLINE } from '../types/statusline-fields';
4
+ import type { TokenUsage, ModelUsageEntry } from '../types/sdk-messages';
5
+
6
+ // Port of skill-statusline helpers
7
+ export function fmtTok(n: number): string {
8
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
9
+ if (n >= 1_000) return `${Math.round(n / 1_000)}k`;
10
+ return String(n);
11
+ }
12
+
13
+ export function fmtDuration(ms: number): string {
14
+ if (ms < 1000) return `${ms}ms`;
15
+ const sec = Math.floor(ms / 1000);
16
+ if (sec < 60) return `${sec}s`;
17
+ const min = Math.floor(sec / 60);
18
+ const rem = sec % 60;
19
+ if (min < 60) return rem > 0 ? `${min}m${rem}s` : `${min}m`;
20
+ const hr = Math.floor(min / 60);
21
+ const remMin = min % 60;
22
+ return `${hr}h${remMin}m`;
23
+ }
24
+
25
+ export function fmtCost(usd: number): string {
26
+ if (usd === 0) return '$0.00';
27
+ if (usd < 0.01) return `$${usd.toFixed(4)}`;
28
+ return `$${usd.toFixed(2)}`;
29
+ }
30
+
31
+ // Parse model ID → clean display name (port of core.sh logic)
32
+ export function parseModelName(modelId: string): string {
33
+ const familyMatch = modelId.match(/^claude-([a-z]+)-/);
34
+ if (!familyMatch) return modelId;
35
+ const family = familyMatch[1].charAt(0).toUpperCase() + familyMatch[1].slice(1);
36
+ const verMatch = modelId.match(/-(\d)-(\d)$/);
37
+ if (verMatch) return `${family} ${verMatch[1]}.${verMatch[2]}`;
38
+ const verDateMatch = modelId.match(/-(\d)-\d{8}$/);
39
+ if (verDateMatch) return `${family} ${verDateMatch[1]}`;
40
+ return family;
41
+ }
42
+
43
+ // Tool name → skill label (port of core.sh case statement)
44
+ const SKILL_MAP: Record<string, string> = {
45
+ Task: 'Agent', Read: 'Read', Write: 'Write', Edit: 'Edit',
46
+ MultiEdit: 'Multi Edit', Glob: 'Search(Files)', Grep: 'Search(Content)',
47
+ Bash: 'Terminal', WebSearch: 'Web Search', WebFetch: 'Web Fetch',
48
+ Skill: 'Skill', AskUserQuestion: 'Asking...', EnterPlanMode: 'Planning',
49
+ ExitPlanMode: 'Plan Ready', TaskCreate: 'Task Create', TaskUpdate: 'Task Update',
50
+ NotebookEdit: 'Notebook',
51
+ };
52
+
53
+ interface StatuslineState extends StatuslineData {
54
+ updateFromInit: (model: string, cwd: string, sessionId: string, permissionMode: string) => void;
55
+ updateFromUsage: (usage: TokenUsage) => void;
56
+ updateFromResult: (data: {
57
+ cost: number; duration: number; numTurns: number;
58
+ usage: TokenUsage; modelUsage?: Record<string, ModelUsageEntry>;
59
+ }) => void;
60
+ updateSkill: (toolName: string) => void;
61
+ markCompaction: (preTokens: number) => void;
62
+ updateGit: (data: {
63
+ branch: string; ghUser: string; ghRepo: string;
64
+ staged: boolean; unstaged: boolean;
65
+ }) => void;
66
+ reset: () => void;
67
+ }
68
+
69
+ export const useStatuslineStore = create<StatuslineState>((set, get) => ({
70
+ ...DEFAULT_STATUSLINE,
71
+
72
+ updateFromInit: (modelId, cwd, sessionId, permissionMode) => {
73
+ const model = parseModelName(modelId);
74
+ const parts = cwd.replace(/\\/g, '/').split('/').filter(Boolean);
75
+ const dirShort = parts.length > 3
76
+ ? parts.slice(-3).join('/')
77
+ : parts.length > 0 ? parts.slice(-2).join('/') : '~';
78
+
79
+ set({ model, modelId, cwd, dirShort, sessionId, permissionMode });
80
+ },
81
+
82
+ updateFromUsage: (usage) => {
83
+ const s = get();
84
+ const inputTokens = usage.input_tokens || 0;
85
+ const outputTokens = usage.output_tokens || 0;
86
+ const cacheCreate = usage.cache_creation_input_tokens || 0;
87
+ const cacheRead = usage.cache_read_input_tokens || 0;
88
+
89
+ // Context % = (input + cache_creation + cache_read) / context_size
90
+ const contextUsed = inputTokens + cacheCreate + cacheRead;
91
+ const contextPct = s.contextSize > 0
92
+ ? Math.min(100, Math.round((contextUsed / s.contextSize) * 100))
93
+ : 0;
94
+
95
+ let compactionWarning = '';
96
+ let isCompacting = false;
97
+ if (contextPct >= 95) {
98
+ compactionWarning = 'COMPACTING';
99
+ isCompacting = true;
100
+ } else if (contextPct >= 85) {
101
+ compactionWarning = `${100 - contextPct}% left`;
102
+ }
103
+
104
+ set({
105
+ tokensWinIn: inputTokens,
106
+ tokensWinOut: outputTokens,
107
+ tokensCumIn: s.tokensCumIn + inputTokens,
108
+ tokensCumOut: s.tokensCumOut + outputTokens,
109
+ cacheCreate,
110
+ cacheRead,
111
+ contextUsed,
112
+ contextPct,
113
+ contextRemaining: 100 - contextPct,
114
+ compactionWarning,
115
+ isCompacting,
116
+ });
117
+ },
118
+
119
+ updateFromResult: (data) => {
120
+ const contextSize = data.modelUsage
121
+ ? Object.values(data.modelUsage)[0]?.contextWindow || 200000
122
+ : 200000;
123
+
124
+ set({
125
+ cost: data.cost,
126
+ costFormatted: fmtCost(data.cost),
127
+ duration: data.duration,
128
+ durationFormatted: fmtDuration(data.duration),
129
+ numTurns: data.numTurns,
130
+ contextSize,
131
+ });
132
+
133
+ // Recompute burn rate
134
+ if (data.duration > 60000 && data.cost > 0) {
135
+ const rate = data.cost / (data.duration / 60000);
136
+ set({ burnRate: `$${rate.toFixed(2)}/m` });
137
+ }
138
+ },
139
+
140
+ updateSkill: (toolName) => {
141
+ set({ skill: SKILL_MAP[toolName] || toolName });
142
+ },
143
+
144
+ markCompaction: (preTokens) => {
145
+ set({ isCompacting: true, compactionWarning: 'COMPACTING' });
146
+ },
147
+
148
+ updateGit: (data) => {
149
+ const github = data.ghRepo
150
+ ? `${data.ghUser}/${data.ghRepo}/${data.branch}`
151
+ : data.branch;
152
+ set({
153
+ branch: data.branch,
154
+ gitDirty: { staged: data.staged, unstaged: data.unstaged },
155
+ github,
156
+ });
157
+ },
158
+
159
+ reset: () => set(DEFAULT_STATUSLINE),
160
+ }));
@@ -0,0 +1,117 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
2
+
3
+ :root {
4
+ --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
5
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
6
+ --radius: 8px;
7
+ --radius-sm: 4px;
8
+ --radius-lg: 12px;
9
+ }
10
+
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ html, body, #root {
18
+ width: 100%;
19
+ height: 100%;
20
+ overflow: hidden;
21
+ }
22
+
23
+ body {
24
+ font-family: var(--font-sans);
25
+ font-size: 14px;
26
+ line-height: 1.5;
27
+ -webkit-font-smoothing: antialiased;
28
+ -moz-osx-font-smoothing: grayscale;
29
+ }
30
+
31
+ /* Scrollbar styling */
32
+ ::-webkit-scrollbar {
33
+ width: 6px;
34
+ height: 6px;
35
+ }
36
+
37
+ ::-webkit-scrollbar-track {
38
+ background: transparent;
39
+ }
40
+
41
+ ::-webkit-scrollbar-thumb {
42
+ background: var(--color-border, #2a2a32);
43
+ border-radius: 3px;
44
+ }
45
+
46
+ ::-webkit-scrollbar-thumb:hover {
47
+ background: var(--color-text-muted, #787882);
48
+ }
49
+
50
+ /* Selection */
51
+ ::selection {
52
+ background: var(--color-accent, #a855f7);
53
+ color: white;
54
+ }
55
+
56
+ /* Code blocks */
57
+ code, pre {
58
+ font-family: var(--font-mono);
59
+ }
60
+
61
+ pre {
62
+ overflow-x: auto;
63
+ border-radius: var(--radius);
64
+ padding: 12px 16px;
65
+ }
66
+
67
+ /* Markdown content */
68
+ .markdown-content h1,
69
+ .markdown-content h2,
70
+ .markdown-content h3 {
71
+ margin-top: 16px;
72
+ margin-bottom: 8px;
73
+ font-weight: 600;
74
+ }
75
+
76
+ .markdown-content p {
77
+ margin-bottom: 8px;
78
+ }
79
+
80
+ .markdown-content ul, .markdown-content ol {
81
+ margin-left: 20px;
82
+ margin-bottom: 8px;
83
+ }
84
+
85
+ .markdown-content code:not(pre code) {
86
+ padding: 2px 6px;
87
+ border-radius: var(--radius-sm);
88
+ font-size: 0.9em;
89
+ }
90
+
91
+ /* Animations */
92
+ @keyframes fadeIn {
93
+ from { opacity: 0; transform: translateY(4px); }
94
+ to { opacity: 1; transform: translateY(0); }
95
+ }
96
+
97
+ @keyframes pulse {
98
+ 0%, 100% { opacity: 1; }
99
+ 50% { opacity: 0.5; }
100
+ }
101
+
102
+ @keyframes slideUp {
103
+ from { opacity: 0; transform: translateY(8px); }
104
+ to { opacity: 1; transform: translateY(0); }
105
+ }
106
+
107
+ .animate-fade-in {
108
+ animation: fadeIn 0.2s ease-out;
109
+ }
110
+
111
+ .animate-pulse {
112
+ animation: pulse 1.5s ease-in-out infinite;
113
+ }
114
+
115
+ .animate-slide-up {
116
+ animation: slideUp 0.3s ease-out;
117
+ }
@@ -0,0 +1,149 @@
1
+ import { create } from 'zustand';
2
+
3
+ export interface ThemeColors {
4
+ skill: string;
5
+ model: string;
6
+ dir: string;
7
+ github: string;
8
+ tokens: string;
9
+ cost: string;
10
+ vim: string;
11
+ agent: string;
12
+ ctxLow: string;
13
+ ctxMed: string;
14
+ ctxHigh: string;
15
+ ctxCrit: string;
16
+ separator: string;
17
+ barEmpty: string;
18
+ barFilled: string;
19
+ label: string;
20
+ gitStaged: string;
21
+ gitUnstaged: string;
22
+ // UI colors
23
+ bg: string;
24
+ bgSurface: string;
25
+ bgPanel: string;
26
+ bgHover: string;
27
+ text: string;
28
+ textMuted: string;
29
+ textDim: string;
30
+ border: string;
31
+ accent: string;
32
+ error: string;
33
+ success: string;
34
+ warning: string;
35
+ }
36
+
37
+ export interface ThemeDefinition {
38
+ name: string;
39
+ id: string;
40
+ colors: ThemeColors;
41
+ }
42
+
43
+ // Ported from themes/default.sh — RGB values extracted from ANSI escape codes
44
+ export const defaultTheme: ThemeDefinition = {
45
+ name: 'Default',
46
+ id: 'default',
47
+ colors: {
48
+ skill: '#ec4899', model: '#a855f7', dir: '#06b6d4', github: '#e4e4e7',
49
+ tokens: '#f59e0b', cost: '#22c55e', vim: '#2dd4bf', agent: '#60a5fa',
50
+ ctxLow: '#e4e4e7', ctxMed: '#fb923c', ctxHigh: '#ef4444', ctxCrit: '#dc2626',
51
+ separator: '#37373e', barEmpty: '#28282d', barFilled: '#e4e4e7',
52
+ label: '#787882', gitStaged: '#22c55e', gitUnstaged: '#f59e0b',
53
+ bg: '#0a0a0f', bgSurface: '#141419', bgPanel: '#1a1a22', bgHover: '#24242e',
54
+ text: '#e4e4e7', textMuted: '#787882', textDim: '#505058',
55
+ border: '#2a2a32', accent: '#a855f7', error: '#ef4444', success: '#22c55e', warning: '#f59e0b',
56
+ },
57
+ };
58
+
59
+ // Ported from themes/nord.sh
60
+ export const nordTheme: ThemeDefinition = {
61
+ name: 'Nord',
62
+ id: 'nord',
63
+ colors: {
64
+ skill: '#88c0d0', model: '#81a1c1', dir: '#8fbcbb', github: '#d8dee9',
65
+ tokens: '#ebcb8b', cost: '#a3be8c', vim: '#88c0d0', agent: '#81a1c1',
66
+ ctxLow: '#d8dee9', ctxMed: '#d08770', ctxHigh: '#bf616a', ctxCrit: '#bf616a',
67
+ separator: '#434c5e', barEmpty: '#3b4252', barFilled: '#d8dee9',
68
+ label: '#616e88', gitStaged: '#a3be8c', gitUnstaged: '#ebcb8b',
69
+ bg: '#2e3440', bgSurface: '#3b4252', bgPanel: '#434c5e', bgHover: '#4c566a',
70
+ text: '#d8dee9', textMuted: '#616e88', textDim: '#4c566a',
71
+ border: '#434c5e', accent: '#81a1c1', error: '#bf616a', success: '#a3be8c', warning: '#ebcb8b',
72
+ },
73
+ };
74
+
75
+ // Ported from themes/tokyo-night.sh
76
+ export const tokyoNightTheme: ThemeDefinition = {
77
+ name: 'Tokyo Night',
78
+ id: 'tokyo-night',
79
+ colors: {
80
+ skill: '#f7768e', model: '#bb9af7', dir: '#7dcfff', github: '#a9b1d6',
81
+ tokens: '#e0af68', cost: '#9ece6a', vim: '#2ac3de', agent: '#7aa2f7',
82
+ ctxLow: '#a9b1d6', ctxMed: '#ff9e64', ctxHigh: '#f7768e', ctxCrit: '#db4b4b',
83
+ separator: '#3b4261', barEmpty: '#1f2335', barFilled: '#a9b1d6',
84
+ label: '#565f89', gitStaged: '#9ece6a', gitUnstaged: '#e0af68',
85
+ bg: '#1a1b26', bgSurface: '#1f2335', bgPanel: '#24283b', bgHover: '#292e42',
86
+ text: '#a9b1d6', textMuted: '#565f89', textDim: '#3b4261',
87
+ border: '#3b4261', accent: '#bb9af7', error: '#f7768e', success: '#9ece6a', warning: '#e0af68',
88
+ },
89
+ };
90
+
91
+ // Ported from themes/catppuccin.sh (Mocha)
92
+ export const catppuccinTheme: ThemeDefinition = {
93
+ name: 'Catppuccin',
94
+ id: 'catppuccin',
95
+ colors: {
96
+ skill: '#f38ba8', model: '#cba6f7', dir: '#89dceb', github: '#cdd6f4',
97
+ tokens: '#f9e2af', cost: '#a6e3a1', vim: '#94e2d5', agent: '#89b4fa',
98
+ ctxLow: '#cdd6f4', ctxMed: '#fab387', ctxHigh: '#f38ba8', ctxCrit: '#eba0ac',
99
+ separator: '#45475a', barEmpty: '#313244', barFilled: '#cdd6f4',
100
+ label: '#6c7086', gitStaged: '#a6e3a1', gitUnstaged: '#f9e2af',
101
+ bg: '#1e1e2e', bgSurface: '#24243a', bgPanel: '#313244', bgHover: '#45475a',
102
+ text: '#cdd6f4', textMuted: '#6c7086', textDim: '#45475a',
103
+ border: '#45475a', accent: '#cba6f7', error: '#f38ba8', success: '#a6e3a1', warning: '#f9e2af',
104
+ },
105
+ };
106
+
107
+ // Ported from themes/gruvbox.sh
108
+ export const gruvboxTheme: ThemeDefinition = {
109
+ name: 'Gruvbox',
110
+ id: 'gruvbox',
111
+ colors: {
112
+ skill: '#d3869b', model: '#b16286', dir: '#83a598', github: '#ebdbb2',
113
+ tokens: '#fabd2f', cost: '#b8bb26', vim: '#83a598', agent: '#83a598',
114
+ ctxLow: '#ebdbb2', ctxMed: '#fe8019', ctxHigh: '#fb4934', ctxCrit: '#cc241d',
115
+ separator: '#504945', barEmpty: '#3c3836', barFilled: '#ebdbb2',
116
+ label: '#7c6f64', gitStaged: '#b8bb26', gitUnstaged: '#fabd2f',
117
+ bg: '#282828', bgSurface: '#3c3836', bgPanel: '#504945', bgHover: '#665c54',
118
+ text: '#ebdbb2', textMuted: '#7c6f64', textDim: '#504945',
119
+ border: '#504945', accent: '#b16286', error: '#fb4934', success: '#b8bb26', warning: '#fabd2f',
120
+ },
121
+ };
122
+
123
+ export const THEMES: ThemeDefinition[] = [
124
+ defaultTheme, nordTheme, tokyoNightTheme, catppuccinTheme, gruvboxTheme,
125
+ ];
126
+
127
+ interface ThemeState {
128
+ activeTheme: ThemeDefinition;
129
+ setTheme: (id: string) => void;
130
+ }
131
+
132
+ export const useThemeStore = create<ThemeState>((set) => ({
133
+ activeTheme: defaultTheme,
134
+ setTheme: (id) => {
135
+ const theme = THEMES.find((t) => t.id === id) || defaultTheme;
136
+ set({ activeTheme: theme });
137
+ // Persist choice
138
+ try { localStorage.setItem('upfynai-theme', id); } catch {}
139
+ },
140
+ }));
141
+
142
+ // Load persisted theme
143
+ try {
144
+ const saved = localStorage.getItem('upfynai-theme');
145
+ if (saved) {
146
+ const theme = THEMES.find((t) => t.id === saved);
147
+ if (theme) useThemeStore.setState({ activeTheme: theme });
148
+ }
149
+ } catch {}
@@ -0,0 +1,24 @@
1
+ // Canvas node/edge types for tldraw visualization
2
+
3
+ export type CanvasNodeType =
4
+ | 'session'
5
+ | 'toolCall'
6
+ | 'toolResult'
7
+ | 'textResponse'
8
+ | 'compaction';
9
+
10
+ export interface CanvasNode {
11
+ id: string;
12
+ type: CanvasNodeType;
13
+ label: string;
14
+ data: Record<string, unknown>;
15
+ x: number;
16
+ y: number;
17
+ timestamp: number;
18
+ }
19
+
20
+ export interface CanvasEdge {
21
+ id: string;
22
+ fromId: string;
23
+ toId: string;
24
+ }