upfynai-code 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/bin/cli.js +86 -0
- package/dist/assets/CanvasPanel-B48gAKVY.js +538 -0
- package/dist/assets/CanvasPanel-B48gAKVY.js.map +1 -0
- package/dist/assets/CanvasPanel-BsOG3EVs.css +1 -0
- package/dist/assets/index-CEhTwG68.css +1 -0
- package/dist/assets/index-GqAGWpJI.js +70 -0
- package/dist/assets/index-GqAGWpJI.js.map +1 -0
- package/dist/index.html +18 -0
- package/index.html +17 -0
- package/package.json +67 -0
- package/src/App.tsx +226 -0
- package/src/components/canvas/CanvasPanel.tsx +62 -0
- package/src/components/canvas/layout/graph-builder.ts +136 -0
- package/src/components/canvas/shapes/CompactionNodeShape.tsx +76 -0
- package/src/components/canvas/shapes/SessionNodeShape.tsx +93 -0
- package/src/components/canvas/shapes/StatuslineWidgetShape.tsx +125 -0
- package/src/components/canvas/shapes/TextResponseNodeShape.tsx +86 -0
- package/src/components/canvas/shapes/ToolCallNodeShape.tsx +107 -0
- package/src/components/canvas/shapes/ToolResultNodeShape.tsx +87 -0
- package/src/components/canvas/shapes/shared-styles.ts +35 -0
- package/src/components/chat/ChatPanel.tsx +96 -0
- package/src/components/chat/InputBar.tsx +81 -0
- package/src/components/chat/MessageList.tsx +130 -0
- package/src/components/chat/PermissionDialog.tsx +70 -0
- package/src/components/layout/FolderSelector.tsx +152 -0
- package/src/components/layout/ModelSelector.tsx +65 -0
- package/src/components/layout/SessionManager.tsx +115 -0
- package/src/components/statusline/StatuslineBar.tsx +114 -0
- package/src/main.tsx +10 -0
- package/src/server/claude-session.ts +156 -0
- package/src/server/index.ts +149 -0
- package/src/services/stream-consumer.ts +330 -0
- package/src/statusline-core/bin/statusline.sh +121 -0
- package/src/statusline-core/commands/sls-config.md +42 -0
- package/src/statusline-core/commands/sls-doctor.md +35 -0
- package/src/statusline-core/commands/sls-help.md +48 -0
- package/src/statusline-core/commands/sls-layout.md +38 -0
- package/src/statusline-core/commands/sls-preview.md +34 -0
- package/src/statusline-core/commands/sls-theme.md +40 -0
- package/src/statusline-core/installer.js +228 -0
- package/src/statusline-core/layouts/compact.sh +21 -0
- package/src/statusline-core/layouts/full.sh +62 -0
- package/src/statusline-core/layouts/standard.sh +39 -0
- package/src/statusline-core/lib/core.sh +389 -0
- package/src/statusline-core/lib/helpers.sh +81 -0
- package/src/statusline-core/lib/json-parser.sh +71 -0
- package/src/statusline-core/themes/catppuccin.sh +32 -0
- package/src/statusline-core/themes/default.sh +37 -0
- package/src/statusline-core/themes/gruvbox.sh +32 -0
- package/src/statusline-core/themes/nord.sh +32 -0
- package/src/statusline-core/themes/tokyo-night.sh +32 -0
- package/src/store/canvas-store.ts +50 -0
- package/src/store/chat-store.ts +60 -0
- package/src/store/permission-store.ts +29 -0
- package/src/store/session-store.ts +52 -0
- package/src/store/statusline-store.ts +160 -0
- package/src/styles/global.css +117 -0
- package/src/themes/index.ts +149 -0
- package/src/types/canvas-graph.ts +24 -0
- package/src/types/sdk-messages.ts +156 -0
- package/src/types/statusline-fields.ts +67 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +24 -0
|
@@ -0,0 +1,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
|
+
}
|