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,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
+ }