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
package/dist/index.html
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>UC UpfynAI-Code — Visual AI Coding Interface</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { background: #0a0a0f; color: #e4e4e7; font-family: 'Inter', system-ui, -apple-system, sans-serif; }
|
|
10
|
+
#root { width: 100vw; height: 100vh; overflow: hidden; }
|
|
11
|
+
</style>
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-GqAGWpJI.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CEhTwG68.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div id="root"></div>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
package/index.html
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>UC UpfynAI-Code — Visual AI Coding Interface</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { background: #0a0a0f; color: #e4e4e7; font-family: 'Inter', system-ui, -apple-system, sans-serif; }
|
|
10
|
+
#root { width: 100vw; height: 100vh; overflow: hidden; }
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="root"></div>
|
|
15
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "upfynai-code",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "UC UpfynAI-Code — Visual AI coding interface with built-in statusline. Proprietary software by Thinqmesh Technologies, Developed by Anit Chaudhary.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"upfynai-code": "bin/cli.js",
|
|
8
|
+
"uc": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "node bin/cli.js",
|
|
12
|
+
"build": "vite build",
|
|
13
|
+
"preview": "vite preview"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"dist/",
|
|
18
|
+
"src/",
|
|
19
|
+
"index.html",
|
|
20
|
+
"vite.config.ts",
|
|
21
|
+
"tsconfig.json"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"claude-code",
|
|
25
|
+
"ai-coding",
|
|
26
|
+
"tldraw",
|
|
27
|
+
"terminal-ui",
|
|
28
|
+
"statusline",
|
|
29
|
+
"anthropic",
|
|
30
|
+
"developer-tools"
|
|
31
|
+
],
|
|
32
|
+
"author": {
|
|
33
|
+
"name": "Anit Chaudhary",
|
|
34
|
+
"url": "https://github.com/AnitChaudhry"
|
|
35
|
+
},
|
|
36
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
37
|
+
"homepage": "https://upfyn.ai",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/AnitChaudhry/UpfynAI-Code.git"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@hono/node-server": "^1.13.0",
|
|
47
|
+
"@tldraw/tldraw": "^4.4.0",
|
|
48
|
+
"hono": "^4.7.0",
|
|
49
|
+
"open": "^10.1.0",
|
|
50
|
+
"react": "^18.3.1",
|
|
51
|
+
"react-dom": "^18.3.1",
|
|
52
|
+
"react-markdown": "^9.0.0",
|
|
53
|
+
"remark-gfm": "^4.0.0",
|
|
54
|
+
"zustand": "^5.0.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^22.0.0",
|
|
58
|
+
"@types/react": "^18.3.0",
|
|
59
|
+
"@types/react-dom": "^18.3.0",
|
|
60
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
61
|
+
"typescript": "^5.7.0",
|
|
62
|
+
"vite": "^6.1.0"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
2
|
+
import { lazy, Suspense } from 'react';
|
|
3
|
+
import { ChatPanel } from './components/chat/ChatPanel';
|
|
4
|
+
import { StatuslineBar } from './components/statusline/StatuslineBar';
|
|
5
|
+
import { FolderSelector } from './components/layout/FolderSelector';
|
|
6
|
+
import { ModelSelector } from './components/layout/ModelSelector';
|
|
7
|
+
import { SessionManager } from './components/layout/SessionManager';
|
|
8
|
+
import { useThemeStore } from './themes';
|
|
9
|
+
import { useSessionStore } from './store/session-store';
|
|
10
|
+
import { useStatuslineStore } from './store/statusline-store';
|
|
11
|
+
import { connectSSE, sendPrompt } from './services/stream-consumer';
|
|
12
|
+
|
|
13
|
+
const CanvasPanel = lazy(() => import('./components/canvas/CanvasPanel').then(m => ({ default: m.CanvasPanel })));
|
|
14
|
+
|
|
15
|
+
export function App() {
|
|
16
|
+
const theme = useThemeStore((s) => s.activeTheme);
|
|
17
|
+
const cwd = useSessionStore((s) => s.cwd);
|
|
18
|
+
const isConnected = useSessionStore((s) => s.isConnected);
|
|
19
|
+
const model = useStatuslineStore((s) => s.model);
|
|
20
|
+
|
|
21
|
+
const [folderOpen, setFolderOpen] = useState(false);
|
|
22
|
+
const [modelOpen, setModelOpen] = useState(false);
|
|
23
|
+
const [sessionOpen, setSessionOpen] = useState(false);
|
|
24
|
+
const [canvasVisible, setCanvasVisible] = useState(true);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const sse = connectSSE();
|
|
28
|
+
return () => sse.close();
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
// Keyboard shortcuts
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
function handleKey(e: KeyboardEvent) {
|
|
34
|
+
if (e.ctrlKey && e.key === 'o') { e.preventDefault(); setFolderOpen(true); }
|
|
35
|
+
if (e.ctrlKey && e.key === 'i') { e.preventDefault(); setSessionOpen(true); }
|
|
36
|
+
if (e.ctrlKey && e.key === 'b') { e.preventDefault(); setCanvasVisible((v) => !v); }
|
|
37
|
+
if (e.key === 'Escape') { setFolderOpen(false); setModelOpen(false); setSessionOpen(false); }
|
|
38
|
+
}
|
|
39
|
+
window.addEventListener('keydown', handleKey);
|
|
40
|
+
return () => window.removeEventListener('keydown', handleKey);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const handleFolderSelect = useCallback((path: string) => {
|
|
44
|
+
useSessionStore.getState().setCwd(path);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const handleModelSelect = useCallback((modelId: string) => {
|
|
48
|
+
useSessionStore.getState().setModel(modelId);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const handleResume = useCallback((sessionId: string) => {
|
|
52
|
+
sendPrompt(`/resume ${sessionId}`);
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
// Apply theme as CSS variables
|
|
56
|
+
const themeVars = {
|
|
57
|
+
'--color-bg': theme.colors.bg,
|
|
58
|
+
'--color-surface': theme.colors.bgSurface,
|
|
59
|
+
'--color-panel': theme.colors.bgPanel,
|
|
60
|
+
'--color-hover': theme.colors.bgHover,
|
|
61
|
+
'--color-text': theme.colors.text,
|
|
62
|
+
'--color-text-muted': theme.colors.textMuted,
|
|
63
|
+
'--color-text-dim': theme.colors.textDim,
|
|
64
|
+
'--color-border': theme.colors.border,
|
|
65
|
+
'--color-accent': theme.colors.accent,
|
|
66
|
+
'--color-error': theme.colors.error,
|
|
67
|
+
'--color-success': theme.colors.success,
|
|
68
|
+
'--color-warning': theme.colors.warning,
|
|
69
|
+
'--color-skill': theme.colors.skill,
|
|
70
|
+
'--color-model': theme.colors.model,
|
|
71
|
+
'--color-dir': theme.colors.dir,
|
|
72
|
+
'--color-github': theme.colors.github,
|
|
73
|
+
'--color-tokens': theme.colors.tokens,
|
|
74
|
+
'--color-cost': theme.colors.cost,
|
|
75
|
+
'--color-ctx-low': theme.colors.ctxLow,
|
|
76
|
+
'--color-ctx-med': theme.colors.ctxMed,
|
|
77
|
+
'--color-ctx-high': theme.colors.ctxHigh,
|
|
78
|
+
'--color-ctx-crit': theme.colors.ctxCrit,
|
|
79
|
+
'--color-bar-empty': theme.colors.barEmpty,
|
|
80
|
+
'--color-bar-filled': theme.colors.barFilled,
|
|
81
|
+
'--color-separator': theme.colors.separator,
|
|
82
|
+
} as React.CSSProperties;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="app-root" style={{ ...themeVars, background: theme.colors.bg, color: theme.colors.text, width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
86
|
+
{/* Header */}
|
|
87
|
+
<header style={{
|
|
88
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
89
|
+
padding: '8px 16px', borderBottom: `1px solid ${theme.colors.border}`,
|
|
90
|
+
background: theme.colors.bgSurface, flexShrink: 0,
|
|
91
|
+
}}>
|
|
92
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
93
|
+
<span style={{
|
|
94
|
+
color: theme.colors.bg, fontWeight: 800, fontSize: 12,
|
|
95
|
+
background: theme.colors.skill, borderRadius: 3, padding: '2px 6px',
|
|
96
|
+
letterSpacing: 1,
|
|
97
|
+
}}>UC</span>
|
|
98
|
+
<span style={{ color: theme.colors.skill, fontWeight: 700, fontSize: 18, marginLeft: 6 }}>UpfynAI</span>
|
|
99
|
+
<span style={{ color: theme.colors.accent, fontWeight: 700, fontSize: 18 }}>-Code</span>
|
|
100
|
+
<span style={{ color: theme.colors.textDim, fontSize: 11, marginLeft: 8 }}>by Thinqmesh Technologies</span>
|
|
101
|
+
</div>
|
|
102
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
103
|
+
{/* Folder selector */}
|
|
104
|
+
<ToolbarButton theme={theme} onClick={() => setFolderOpen(true)} title="Open Project (Ctrl+O)">
|
|
105
|
+
<span style={{ fontSize: 14 }}>📂</span>
|
|
106
|
+
<span style={{ maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
107
|
+
{cwd ? cwd.split(/[/\\]/).pop() : 'Open'}
|
|
108
|
+
</span>
|
|
109
|
+
</ToolbarButton>
|
|
110
|
+
|
|
111
|
+
{/* Model selector */}
|
|
112
|
+
<div style={{ position: 'relative' }}>
|
|
113
|
+
<ToolbarButton theme={theme} onClick={() => setModelOpen(!modelOpen)} title="Select Model">
|
|
114
|
+
<span style={{ width: 6, height: 6, borderRadius: '50%', background: theme.colors.model, flexShrink: 0 }} />
|
|
115
|
+
<span>{model || 'Model'}</span>
|
|
116
|
+
</ToolbarButton>
|
|
117
|
+
<ModelSelector isOpen={modelOpen} onClose={() => setModelOpen(false)} onSelect={handleModelSelect} />
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Session info */}
|
|
121
|
+
<ToolbarButton theme={theme} onClick={() => setSessionOpen(true)} title="Session Info (Ctrl+I)">
|
|
122
|
+
<ConnectionDot connected={isConnected} />
|
|
123
|
+
<span>Session</span>
|
|
124
|
+
</ToolbarButton>
|
|
125
|
+
|
|
126
|
+
{/* Toggle canvas */}
|
|
127
|
+
<ToolbarButton theme={theme} onClick={() => setCanvasVisible(!canvasVisible)} title="Toggle Canvas (Ctrl+B)">
|
|
128
|
+
<span style={{ fontSize: 13 }}>{canvasVisible ? '◧' : '▣'}</span>
|
|
129
|
+
</ToolbarButton>
|
|
130
|
+
|
|
131
|
+
{/* Theme picker */}
|
|
132
|
+
<ThemePicker />
|
|
133
|
+
</div>
|
|
134
|
+
</header>
|
|
135
|
+
|
|
136
|
+
{/* Main content */}
|
|
137
|
+
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
|
138
|
+
<ChatPanel />
|
|
139
|
+
{/* Canvas panel — tldraw agent workflow visualization */}
|
|
140
|
+
{canvasVisible && (
|
|
141
|
+
<div style={{
|
|
142
|
+
width: '40%', borderLeft: `1px solid ${theme.colors.border}`,
|
|
143
|
+
minWidth: 300, position: 'relative',
|
|
144
|
+
}}>
|
|
145
|
+
<Suspense fallback={
|
|
146
|
+
<div style={{
|
|
147
|
+
width: '100%', height: '100%', display: 'flex',
|
|
148
|
+
alignItems: 'center', justifyContent: 'center',
|
|
149
|
+
background: theme.colors.bgPanel, color: theme.colors.textDim,
|
|
150
|
+
}}>
|
|
151
|
+
Loading canvas...
|
|
152
|
+
</div>
|
|
153
|
+
}>
|
|
154
|
+
<CanvasPanel />
|
|
155
|
+
</Suspense>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* StatuslineBar — CORE COMPONENT: Always rendered, cannot be toggled or hidden */}
|
|
161
|
+
<StatuslineBar />
|
|
162
|
+
|
|
163
|
+
{/* Modals */}
|
|
164
|
+
<FolderSelector
|
|
165
|
+
currentCwd={cwd}
|
|
166
|
+
onSelect={handleFolderSelect}
|
|
167
|
+
isOpen={folderOpen}
|
|
168
|
+
onClose={() => setFolderOpen(false)}
|
|
169
|
+
/>
|
|
170
|
+
<SessionManager
|
|
171
|
+
isOpen={sessionOpen}
|
|
172
|
+
onClose={() => setSessionOpen(false)}
|
|
173
|
+
onResume={handleResume}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function ToolbarButton({ theme, onClick, title, children }: {
|
|
180
|
+
theme: any; onClick: () => void; title: string; children: React.ReactNode;
|
|
181
|
+
}) {
|
|
182
|
+
return (
|
|
183
|
+
<button onClick={onClick} title={title} style={{
|
|
184
|
+
display: 'flex', alignItems: 'center', gap: 6,
|
|
185
|
+
background: theme.colors.bgPanel, color: theme.colors.text,
|
|
186
|
+
border: `1px solid ${theme.colors.border}`, borderRadius: 6,
|
|
187
|
+
padding: '5px 10px', fontSize: 12, cursor: 'pointer',
|
|
188
|
+
whiteSpace: 'nowrap',
|
|
189
|
+
}}>
|
|
190
|
+
{children}
|
|
191
|
+
</button>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ConnectionDot({ connected }: { connected: boolean }) {
|
|
196
|
+
return (
|
|
197
|
+
<span style={{
|
|
198
|
+
width: 7, height: 7, borderRadius: '50%',
|
|
199
|
+
background: connected ? '#22c55e' : '#ef4444',
|
|
200
|
+
flexShrink: 0, boxShadow: connected ? '0 0 4px #22c55e' : 'none',
|
|
201
|
+
}} />
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function ThemePicker() {
|
|
206
|
+
const { activeTheme, setTheme } = useThemeStore();
|
|
207
|
+
const themes = ['default', 'nord', 'tokyo-night', 'catppuccin', 'gruvbox'];
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<select
|
|
211
|
+
value={activeTheme.id}
|
|
212
|
+
onChange={(e) => setTheme(e.target.value)}
|
|
213
|
+
style={{
|
|
214
|
+
background: activeTheme.colors.bgPanel,
|
|
215
|
+
color: activeTheme.colors.text,
|
|
216
|
+
border: `1px solid ${activeTheme.colors.border}`,
|
|
217
|
+
borderRadius: 4, padding: '4px 8px', fontSize: 12,
|
|
218
|
+
cursor: 'pointer', outline: 'none',
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
{themes.map((t) => (
|
|
222
|
+
<option key={t} value={t}>{t.charAt(0).toUpperCase() + t.slice(1).replace('-', ' ')}</option>
|
|
223
|
+
))}
|
|
224
|
+
</select>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { Tldraw, type Editor } from '@tldraw/tldraw';
|
|
3
|
+
import '@tldraw/tldraw/tldraw.css';
|
|
4
|
+
import { SessionNodeShapeUtil } from './shapes/SessionNodeShape';
|
|
5
|
+
import { ToolCallNodeShapeUtil } from './shapes/ToolCallNodeShape';
|
|
6
|
+
import { ToolResultNodeShapeUtil } from './shapes/ToolResultNodeShape';
|
|
7
|
+
import { TextResponseNodeShapeUtil } from './shapes/TextResponseNodeShape';
|
|
8
|
+
import { CompactionNodeShapeUtil } from './shapes/CompactionNodeShape';
|
|
9
|
+
import { StatuslineWidgetShapeUtil } from './shapes/StatuslineWidgetShape';
|
|
10
|
+
import { syncCanvasToEditor } from './layout/graph-builder';
|
|
11
|
+
import { useCanvasStore } from '../../store/canvas-store';
|
|
12
|
+
import { useStatuslineStore } from '../../store/statusline-store';
|
|
13
|
+
|
|
14
|
+
// Register custom shapes alongside tldraw's built-in shapes
|
|
15
|
+
const customShapeUtils = [
|
|
16
|
+
SessionNodeShapeUtil,
|
|
17
|
+
ToolCallNodeShapeUtil,
|
|
18
|
+
ToolResultNodeShapeUtil,
|
|
19
|
+
TextResponseNodeShapeUtil,
|
|
20
|
+
CompactionNodeShapeUtil,
|
|
21
|
+
StatuslineWidgetShapeUtil,
|
|
22
|
+
] as any[];
|
|
23
|
+
|
|
24
|
+
export function CanvasPanel() {
|
|
25
|
+
const editorRef = useRef<Editor | null>(null);
|
|
26
|
+
const nodes = useCanvasStore((s) => s.nodes);
|
|
27
|
+
const contextPct = useStatuslineStore((s) => s.contextPct);
|
|
28
|
+
const cost = useStatuslineStore((s) => s.costFormatted);
|
|
29
|
+
const skill = useStatuslineStore((s) => s.skill);
|
|
30
|
+
|
|
31
|
+
const handleMount = useCallback((editor: Editor) => {
|
|
32
|
+
editorRef.current = editor;
|
|
33
|
+
// Set dark mode via official API
|
|
34
|
+
editor.user.updateUserPreferences({ colorScheme: 'dark' });
|
|
35
|
+
// Initial sync of agent workflow nodes
|
|
36
|
+
syncCanvasToEditor(editor);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
// Sync custom agent nodes on state changes
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (editorRef.current) {
|
|
42
|
+
syncCanvasToEditor(editorRef.current);
|
|
43
|
+
}
|
|
44
|
+
}, [nodes, contextPct, cost, skill]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div style={{ width: '100%', height: '100%' }}>
|
|
48
|
+
{/*
|
|
49
|
+
Official tldraw component — all core features preserved:
|
|
50
|
+
- Full toolbar (draw, select, eraser, arrow, text, shapes, etc.)
|
|
51
|
+
- Zoom controls, minimap, page management
|
|
52
|
+
- Undo/redo, keyboard shortcuts
|
|
53
|
+
- Export, copy/paste, multi-select
|
|
54
|
+
- Custom shapes are ADDED alongside built-in shapes
|
|
55
|
+
*/}
|
|
56
|
+
<Tldraw
|
|
57
|
+
shapeUtils={customShapeUtils}
|
|
58
|
+
onMount={handleMount}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Editor } from '@tldraw/tldraw';
|
|
2
|
+
import { useCanvasStore } from '../../../store/canvas-store';
|
|
3
|
+
import { useStatuslineStore, fmtTok } from '../../../store/statusline-store';
|
|
4
|
+
import type { CanvasNode } from '../../../types/canvas-graph';
|
|
5
|
+
|
|
6
|
+
const COLUMN_X = 120;
|
|
7
|
+
const START_Y = 120;
|
|
8
|
+
const SPACING_Y = 100;
|
|
9
|
+
const WIDGET_X = 500;
|
|
10
|
+
const WIDGET_Y = 60;
|
|
11
|
+
|
|
12
|
+
let shapeCount = 0;
|
|
13
|
+
|
|
14
|
+
export function syncCanvasToEditor(editor: Editor) {
|
|
15
|
+
const { nodes } = useCanvasStore.getState();
|
|
16
|
+
const sl = useStatuslineStore.getState();
|
|
17
|
+
|
|
18
|
+
// Collect existing shape IDs
|
|
19
|
+
const existingIds = new Set(
|
|
20
|
+
editor.getCurrentPageShapeIds()
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Add new nodes as shapes
|
|
24
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
25
|
+
const node = nodes[i];
|
|
26
|
+
const shapeId = `shape:${node.id}` as any;
|
|
27
|
+
|
|
28
|
+
if (existingIds.has(shapeId)) continue;
|
|
29
|
+
|
|
30
|
+
const y = START_Y + i * SPACING_Y;
|
|
31
|
+
|
|
32
|
+
switch (node.type) {
|
|
33
|
+
case 'session':
|
|
34
|
+
editor.createShape({
|
|
35
|
+
id: shapeId,
|
|
36
|
+
type: 'sessionNode' as any,
|
|
37
|
+
x: COLUMN_X,
|
|
38
|
+
y,
|
|
39
|
+
props: {
|
|
40
|
+
model: (node.data as any).model || 'unknown',
|
|
41
|
+
cwd: (node.data as any).cwd || '',
|
|
42
|
+
sessionId: (node.data as any).session_id || '',
|
|
43
|
+
toolCount: (node.data as any).tools?.length || 0,
|
|
44
|
+
},
|
|
45
|
+
} as any);
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
case 'toolCall':
|
|
49
|
+
editor.createShape({
|
|
50
|
+
id: shapeId,
|
|
51
|
+
type: 'toolCallNode' as any,
|
|
52
|
+
x: COLUMN_X,
|
|
53
|
+
y,
|
|
54
|
+
props: {
|
|
55
|
+
toolName: (node.data as any).name || '',
|
|
56
|
+
toolInput: JSON.stringify((node.data as any).input || {}).slice(0, 200),
|
|
57
|
+
status: 'complete',
|
|
58
|
+
},
|
|
59
|
+
} as any);
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case 'toolResult':
|
|
63
|
+
editor.createShape({
|
|
64
|
+
id: shapeId,
|
|
65
|
+
type: 'toolResultNode' as any,
|
|
66
|
+
x: COLUMN_X + 20,
|
|
67
|
+
y,
|
|
68
|
+
props: {
|
|
69
|
+
toolName: (node.data as any).toolName || '',
|
|
70
|
+
outputSummary: String((node.data as any).content || '').slice(0, 200),
|
|
71
|
+
isError: !!(node.data as any).is_error,
|
|
72
|
+
},
|
|
73
|
+
} as any);
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
case 'textResponse':
|
|
77
|
+
editor.createShape({
|
|
78
|
+
id: shapeId,
|
|
79
|
+
type: 'textResponseNode' as any,
|
|
80
|
+
x: COLUMN_X,
|
|
81
|
+
y,
|
|
82
|
+
props: {
|
|
83
|
+
text: (node.data as any).text || '',
|
|
84
|
+
},
|
|
85
|
+
} as any);
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case 'compaction':
|
|
89
|
+
editor.createShape({
|
|
90
|
+
id: shapeId,
|
|
91
|
+
type: 'compactionNode' as any,
|
|
92
|
+
x: COLUMN_X,
|
|
93
|
+
y,
|
|
94
|
+
props: {
|
|
95
|
+
preTokens: (node.data as any).pre_tokens || 0,
|
|
96
|
+
trigger: (node.data as any).trigger || 'auto',
|
|
97
|
+
},
|
|
98
|
+
} as any);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Update or create statusline widget
|
|
104
|
+
updateStatuslineWidget(editor, sl);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function updateStatuslineWidget(editor: Editor, sl: ReturnType<typeof useStatuslineStore.getState>) {
|
|
108
|
+
const widgetId = 'shape:statusline-widget' as any;
|
|
109
|
+
|
|
110
|
+
const props = {
|
|
111
|
+
model: sl.model,
|
|
112
|
+
cost: sl.costFormatted,
|
|
113
|
+
tokensIn: fmtTok(sl.tokensCumIn),
|
|
114
|
+
tokensOut: fmtTok(sl.tokensCumOut),
|
|
115
|
+
contextPct: sl.contextPct,
|
|
116
|
+
skill: sl.skill,
|
|
117
|
+
turns: sl.numTurns,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const existing = editor.getShape(widgetId);
|
|
121
|
+
if (existing) {
|
|
122
|
+
editor.updateShape({
|
|
123
|
+
id: widgetId,
|
|
124
|
+
type: 'statuslineWidget' as any,
|
|
125
|
+
props,
|
|
126
|
+
} as any);
|
|
127
|
+
} else {
|
|
128
|
+
editor.createShape({
|
|
129
|
+
id: widgetId,
|
|
130
|
+
type: 'statuslineWidget' as any,
|
|
131
|
+
x: WIDGET_X,
|
|
132
|
+
y: WIDGET_Y,
|
|
133
|
+
props,
|
|
134
|
+
} as any);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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 CompactionNodeShape = TLBaseShape<
|
|
9
|
+
'compactionNode',
|
|
10
|
+
{
|
|
11
|
+
w: number;
|
|
12
|
+
h: number;
|
|
13
|
+
preTokens: number;
|
|
14
|
+
trigger: string;
|
|
15
|
+
}
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
// @ts-expect-error — custom shape type not in tldraw's built-in TLShape union
|
|
19
|
+
export class CompactionNodeShapeUtil extends BaseBoxShapeUtil<CompactionNodeShape> {
|
|
20
|
+
static type = 'compactionNode' as const;
|
|
21
|
+
|
|
22
|
+
getDefaultProps(): CompactionNodeShape['props'] {
|
|
23
|
+
return {
|
|
24
|
+
w: 260,
|
|
25
|
+
h: 48,
|
|
26
|
+
preTokens: 0,
|
|
27
|
+
trigger: 'auto',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
component(shape: CompactionNodeShape) {
|
|
32
|
+
const color = NODE_COLORS.compaction;
|
|
33
|
+
const tokensK = Math.round(shape.props.preTokens / 1000);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<HTMLContainer>
|
|
37
|
+
<div
|
|
38
|
+
style={{
|
|
39
|
+
width: shape.props.w,
|
|
40
|
+
height: shape.props.h,
|
|
41
|
+
background: `${color}15`,
|
|
42
|
+
border: `1.5px dashed ${color}60`,
|
|
43
|
+
borderRadius: SHAPE_DEFAULTS.borderRadius,
|
|
44
|
+
padding: '8px 12px',
|
|
45
|
+
fontFamily: 'Inter, system-ui, sans-serif',
|
|
46
|
+
display: 'flex',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
gap: 8,
|
|
49
|
+
pointerEvents: 'all',
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<span style={{ fontSize: 14 }}>⚠️</span>
|
|
53
|
+
<div>
|
|
54
|
+
<div style={{ color, fontSize: 11, fontWeight: 700 }}>
|
|
55
|
+
CONTEXT COMPACTED
|
|
56
|
+
</div>
|
|
57
|
+
<div style={{ color: '#787882', fontSize: 10 }}>
|
|
58
|
+
Was {tokensK}k tokens — {shape.props.trigger}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</HTMLContainer>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
indicator(shape: CompactionNodeShape) {
|
|
67
|
+
return (
|
|
68
|
+
<rect
|
|
69
|
+
width={shape.props.w}
|
|
70
|
+
height={shape.props.h}
|
|
71
|
+
rx={SHAPE_DEFAULTS.borderRadius}
|
|
72
|
+
ry={SHAPE_DEFAULTS.borderRadius}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|