@comergehq/studio 0.1.26 → 0.1.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comergehq/studio",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Comerge studio",
5
5
  "main": "src/index.ts",
6
6
  "module": "dist/index.mjs",
@@ -68,6 +68,7 @@
68
68
  "react-native": "*",
69
69
  "react-native-safe-area-context": "*",
70
70
  "react-native-svg": "*",
71
+ "mixpanel-react-native": "*",
71
72
  "react-native-zip-archive": "*",
72
73
  "react-native-view-shot": "*"
73
74
  },
@@ -0,0 +1,82 @@
1
+ import * as React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import type { AgentRunProgressView } from '../../studio/hooks/useAgentRunProgress';
5
+ import { useTheme } from '../../theme';
6
+ import { Text } from '../primitives/Text';
7
+ import { withAlpha } from '../utils/color';
8
+
9
+ export type AgentProgressCardProps = {
10
+ progress: AgentRunProgressView;
11
+ };
12
+
13
+ function titleForPhase(phase: AgentRunProgressView['phase']): string {
14
+ if (phase === 'planning') return 'Planning';
15
+ if (phase === 'reasoning') return 'Reasoning';
16
+ if (phase === 'analyzing') return 'Analyzing';
17
+ if (phase === 'editing') return 'Editing';
18
+ if (phase === 'executing') return 'Executing';
19
+ if (phase === 'validating') return 'Validating';
20
+ if (phase === 'finalizing') return 'Finalizing';
21
+ if (phase === 'working') return 'Working';
22
+ return 'Working';
23
+ }
24
+
25
+ function titleForStatus(status: AgentRunProgressView['status']): string {
26
+ if (status === 'succeeded') return 'Completed';
27
+ if (status === 'failed') return 'Failed';
28
+ if (status === 'cancelled') return 'Cancelled';
29
+ return 'In Progress';
30
+ }
31
+
32
+ export function AgentProgressCard({ progress }: AgentProgressCardProps) {
33
+ const theme = useTheme();
34
+ const statusLabel = titleForStatus(progress.status);
35
+ const phaseLabel = titleForPhase(progress.phase);
36
+ const subtitle = progress.latestMessage || `Agent is ${phaseLabel.toLowerCase()}...`;
37
+ const todo = progress.todoSummary;
38
+
39
+ return (
40
+ <View
41
+ style={{
42
+ borderWidth: 1,
43
+ borderColor: theme.colors.border,
44
+ borderRadius: theme.radii.lg,
45
+ marginHorizontal: theme.spacing.md,
46
+ padding: theme.spacing.md,
47
+ backgroundColor: withAlpha(theme.colors.surface, theme.scheme === 'dark' ? 0.84 : 0.94),
48
+ }}
49
+ >
50
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
51
+ <Text variant="caption">{statusLabel}</Text>
52
+ <Text variant="captionMuted">{phaseLabel}</Text>
53
+ </View>
54
+
55
+ <Text variant="bodyMuted">{subtitle}</Text>
56
+
57
+ {progress.changedFilesCount > 0 ? (
58
+ <Text variant="captionMuted" style={{ marginTop: 8 }}>
59
+ Updated files: {progress.changedFilesCount}
60
+ </Text>
61
+ ) : null}
62
+
63
+ {progress.recentFiles.length > 0 ? (
64
+ <View style={{ marginTop: 6 }}>
65
+ {progress.recentFiles.map((path) => (
66
+ <Text key={path} variant="captionMuted" numberOfLines={1}>
67
+ • {path}
68
+ </Text>
69
+ ))}
70
+ </View>
71
+ ) : null}
72
+
73
+ {todo ? (
74
+ <Text variant="captionMuted" style={{ marginTop: 8 }}>
75
+ Todos: {todo.completed}/{todo.total} complete
76
+ {todo.currentTask ? ` • ${todo.currentTask}` : ''}
77
+ </Text>
78
+ ) : null}
79
+ </View>
80
+ );
81
+ }
82
+
@@ -0,0 +1,75 @@
1
+ import * as React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import type { AgentBundleProgressView } from '../../studio/hooks/useAgentRunProgress';
5
+ import { useTheme } from '../../theme';
6
+ import { Text } from '../primitives/Text';
7
+ import { withAlpha } from '../utils/color';
8
+
9
+ export type BundleProgressCardProps = {
10
+ progress: AgentBundleProgressView;
11
+ };
12
+
13
+ function titleForStatus(status: AgentBundleProgressView['status']): string {
14
+ if (status === 'succeeded') return 'Completed';
15
+ if (status === 'failed') return 'Failed';
16
+ return 'In Progress';
17
+ }
18
+
19
+ export function BundleProgressCard({ progress }: BundleProgressCardProps) {
20
+ const theme = useTheme();
21
+ const statusLabel = titleForStatus(progress.status);
22
+ const percent = Math.round(Math.max(0, Math.min(1, progress.progressValue)) * 100);
23
+ const fillColor =
24
+ progress.status === 'failed'
25
+ ? theme.colors.danger
26
+ : progress.status === 'succeeded'
27
+ ? theme.colors.success
28
+ : theme.colors.warning;
29
+ const detail = progress.errorMessage || progress.phaseLabel;
30
+
31
+ return (
32
+ <View
33
+ accessible
34
+ accessibilityRole="progressbar"
35
+ accessibilityLabel={`Bundle progress ${statusLabel}`}
36
+ accessibilityValue={{ min: 0, max: 100, now: percent, text: `${percent}%` }}
37
+ style={{
38
+ borderWidth: 1,
39
+ borderColor: theme.colors.border,
40
+ borderRadius: theme.radii.lg,
41
+ marginHorizontal: theme.spacing.md,
42
+ padding: theme.spacing.md,
43
+ backgroundColor: withAlpha(theme.colors.surface, theme.scheme === 'dark' ? 0.84 : 0.94),
44
+ }}
45
+ >
46
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
47
+ <Text variant="caption">{statusLabel}</Text>
48
+ <Text variant="captionMuted">{percent}%</Text>
49
+ </View>
50
+
51
+ <View
52
+ style={{
53
+ width: '100%',
54
+ height: 8,
55
+ borderRadius: 999,
56
+ backgroundColor: withAlpha(theme.colors.border, theme.scheme === 'dark' ? 0.5 : 0.6),
57
+ overflow: 'hidden',
58
+ }}
59
+ >
60
+ <View
61
+ style={{
62
+ width: `${percent}%`,
63
+ height: '100%',
64
+ backgroundColor: fillColor,
65
+ }}
66
+ />
67
+ </View>
68
+
69
+ <Text variant="captionMuted" numberOfLines={1} style={{ marginTop: 8, minHeight: 16 }}>
70
+ {detail}
71
+ </Text>
72
+ </View>
73
+ );
74
+ }
75
+
@@ -17,11 +17,33 @@ export type ChatMessageBubbleProps = {
17
17
  renderContent?: (message: ChatMessage) => React.ReactNode;
18
18
  isLast?: boolean;
19
19
  retrying?: boolean;
20
- onRetry?: () => void;
20
+ onRetryMessage?: (messageId: string) => void;
21
21
  style?: ViewStyle;
22
22
  };
23
23
 
24
- export function ChatMessageBubble({ message, renderContent, isLast, retrying, onRetry, style }: ChatMessageBubbleProps) {
24
+ function areMessageMetaEqual(a: ChatMessage['meta'], b: ChatMessage['meta']): boolean {
25
+ if (a === b) return true;
26
+ if (!a || !b) return a === b;
27
+ return (
28
+ a.kind === b.kind &&
29
+ a.event === b.event &&
30
+ a.status === b.status &&
31
+ a.mergeRequestId === b.mergeRequestId &&
32
+ a.sourceAppId === b.sourceAppId &&
33
+ a.targetAppId === b.targetAppId &&
34
+ a.appId === b.appId &&
35
+ a.threadId === b.threadId
36
+ );
37
+ }
38
+
39
+ function ChatMessageBubbleInner({
40
+ message,
41
+ renderContent,
42
+ isLast,
43
+ retrying,
44
+ onRetryMessage,
45
+ style,
46
+ }: ChatMessageBubbleProps) {
25
47
  const theme = useTheme();
26
48
  const metaEvent = message.meta?.event ?? null;
27
49
  const metaStatus = message.meta?.status ?? null;
@@ -41,8 +63,11 @@ export function ChatMessageBubble({ message, renderContent, isLast, retrying, on
41
63
 
42
64
  const bodyColor =
43
65
  metaStatus === 'success' ? theme.colors.success : metaStatus === 'error' ? theme.colors.danger : undefined;
44
- const showRetry = Boolean(onRetry) && isLast && metaStatus === 'error' && message.author === 'human';
66
+ const showRetry = Boolean(onRetryMessage) && isLast && metaStatus === 'error' && message.author === 'human';
45
67
  const retryLabel = retrying ? 'Retrying...' : 'Retry';
68
+ const handleRetryPress = React.useCallback(() => {
69
+ onRetryMessage?.(message.id);
70
+ }, [message.id, onRetryMessage]);
46
71
 
47
72
  return (
48
73
  <View style={[align, style]}>
@@ -77,7 +102,7 @@ export function ChatMessageBubble({ message, renderContent, isLast, retrying, on
77
102
  <Button
78
103
  variant="ghost"
79
104
  size="sm"
80
- onPress={onRetry}
105
+ onPress={handleRetryPress}
81
106
  disabled={retrying}
82
107
  style={{ borderColor: theme.colors.danger }}
83
108
  accessibilityLabel="Retry send"
@@ -100,4 +125,23 @@ export function ChatMessageBubble({ message, renderContent, isLast, retrying, on
100
125
  );
101
126
  }
102
127
 
128
+ function areEqual(prev: ChatMessageBubbleProps, next: ChatMessageBubbleProps): boolean {
129
+ return (
130
+ prev.message.id === next.message.id &&
131
+ prev.message.author === next.message.author &&
132
+ prev.message.content === next.message.content &&
133
+ prev.message.kind === next.message.kind &&
134
+ String(prev.message.createdAt) === String(next.message.createdAt) &&
135
+ areMessageMetaEqual(prev.message.meta, next.message.meta) &&
136
+ prev.renderContent === next.renderContent &&
137
+ prev.isLast === next.isLast &&
138
+ prev.retrying === next.retrying &&
139
+ prev.onRetryMessage === next.onRetryMessage &&
140
+ prev.style === next.style
141
+ );
142
+ }
143
+
144
+ export const ChatMessageBubble = React.memo(ChatMessageBubbleInner, areEqual);
145
+ ChatMessageBubble.displayName = 'ChatMessageBubble';
146
+
103
147
 
@@ -54,6 +54,7 @@ export const ChatMessageList = React.forwardRef<ChatMessageListRef, ChatMessageL
54
54
  return [...messages].reverse();
55
55
  }, [messages]);
56
56
  const lastMessageId = messages.length > 0 ? messages[messages.length - 1]!.id : null;
57
+ const keyExtractor = React.useCallback((m: ChatMessage) => m.id, []);
57
58
 
58
59
  const scrollToBottom = React.useCallback((options?: { animated?: boolean }) => {
59
60
  const animated = options?.animated ?? true;
@@ -99,51 +100,70 @@ export const ChatMessageList = React.forwardRef<ChatMessageListRef, ChatMessageL
99
100
  return undefined;
100
101
  }, [showTypingIndicator, scrollToBottom]);
101
102
 
103
+ const handleContentSizeChange = React.useCallback(() => {
104
+ if (initialScrollDoneRef.current) return;
105
+ initialScrollDoneRef.current = true;
106
+ lastMessageIdRef.current = messages.length > 0 ? messages[messages.length - 1]!.id : null;
107
+ nearBottomRef.current = true;
108
+ onNearBottomChange?.(true);
109
+ requestAnimationFrame(() => scrollToBottom({ animated: false }));
110
+ }, [messages, onNearBottomChange, scrollToBottom]);
111
+
112
+ const contentContainerStyle = React.useMemo(
113
+ () => [
114
+ {
115
+ paddingHorizontal: theme.spacing.lg,
116
+ paddingVertical: theme.spacing.sm,
117
+ },
118
+ contentStyle,
119
+ ],
120
+ [contentStyle, theme.spacing.lg, theme.spacing.sm]
121
+ );
122
+
123
+ const renderSeparator = React.useCallback(() => <View style={{ height: theme.spacing.sm }} />, [theme.spacing.sm]);
124
+
125
+ const listHeader = React.useMemo(
126
+ () => (
127
+ <View>
128
+ {showTypingIndicator ? (
129
+ <View style={{ marginTop: theme.spacing.sm, alignSelf: 'flex-start', paddingHorizontal: theme.spacing.lg }}>
130
+ <TypingIndicator />
131
+ </View>
132
+ ) : null}
133
+ {bottomInset > 0 ? <View style={{ height: bottomInset }} /> : null}
134
+ </View>
135
+ ),
136
+ [bottomInset, showTypingIndicator, theme.spacing.lg, theme.spacing.sm]
137
+ );
138
+
139
+ const renderItem = React.useCallback(
140
+ ({ item }: { item: ChatMessage }) => (
141
+ <ChatMessageBubble
142
+ message={item}
143
+ renderContent={renderMessageContent}
144
+ isLast={Boolean(lastMessageId && item.id === lastMessageId)}
145
+ retrying={isRetryingMessage?.(item.id) ?? false}
146
+ onRetryMessage={onRetryMessage}
147
+ />
148
+ ),
149
+ [isRetryingMessage, lastMessageId, onRetryMessage, renderMessageContent]
150
+ );
151
+
102
152
  return (
103
153
  <BottomSheetFlatList
104
154
  ref={listRef}
105
155
  inverted
106
156
  data={data}
107
- keyExtractor={(m: ChatMessage) => m.id}
157
+ keyExtractor={keyExtractor}
108
158
  keyboardShouldPersistTaps="handled"
109
159
  onScroll={handleScroll}
110
160
  scrollEventThrottle={16}
111
161
  showsVerticalScrollIndicator={false}
112
- onContentSizeChange={() => {
113
- if (initialScrollDoneRef.current) return;
114
- initialScrollDoneRef.current = true;
115
- lastMessageIdRef.current = messages.length > 0 ? messages[messages.length - 1]!.id : null;
116
- nearBottomRef.current = true;
117
- onNearBottomChange?.(true);
118
- requestAnimationFrame(() => scrollToBottom({ animated: false }));
119
- }}
120
- contentContainerStyle={[
121
- {
122
- paddingHorizontal: theme.spacing.lg,
123
- paddingVertical: theme.spacing.sm,
124
- },
125
- contentStyle,
126
- ]}
127
- ItemSeparatorComponent={() => <View style={{ height: theme.spacing.sm }} />}
128
- renderItem={({ item }: { item: ChatMessage }) => (
129
- <ChatMessageBubble
130
- message={item}
131
- renderContent={renderMessageContent}
132
- isLast={Boolean(lastMessageId && item.id === lastMessageId)}
133
- retrying={isRetryingMessage?.(item.id) ?? false}
134
- onRetry={onRetryMessage ? () => onRetryMessage(item.id) : undefined}
135
- />
136
- )}
137
- ListHeaderComponent={
138
- <View>
139
- {showTypingIndicator ? (
140
- <View style={{ marginTop: theme.spacing.sm, alignSelf: 'flex-start', paddingHorizontal: theme.spacing.lg }}>
141
- <TypingIndicator />
142
- </View>
143
- ) : null}
144
- {bottomInset > 0 ? <View style={{ height: bottomInset }} /> : null}
145
- </View>
146
- }
162
+ onContentSizeChange={handleContentSizeChange}
163
+ contentContainerStyle={contentContainerStyle}
164
+ ItemSeparatorComponent={renderSeparator}
165
+ renderItem={renderItem}
166
+ ListHeaderComponent={listHeader}
147
167
  />
148
168
  );
149
169
  }
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
 
3
3
  import { appCommentsRepository } from '../../data/comments/repository';
4
4
  import type { AppComment } from '../../data/comments/types';
5
+ import { trackSubmitComment } from '../../studio/analytics/track';
5
6
 
6
7
  export type UseAppCommentsResult = {
7
8
  comments: AppComment[];
@@ -58,7 +59,18 @@ export function useAppComments(appId: string | null): UseAppCommentsResult {
58
59
  try {
59
60
  const newComment = await appCommentsRepository.create(appId, { body: trimmed, commentType: 'general' });
60
61
  setComments((prev) => sortByCreatedAtAsc([...prev, newComment]));
62
+ await trackSubmitComment({
63
+ appId,
64
+ commentLength: trimmed.length,
65
+ success: true,
66
+ });
61
67
  } catch (e) {
68
+ await trackSubmitComment({
69
+ appId,
70
+ commentLength: trimmed.length,
71
+ success: false,
72
+ error: e,
73
+ });
62
74
  setError(e instanceof Error ? e : new Error(String(e)));
63
75
  throw e;
64
76
  } finally {
@@ -0,0 +1,179 @@
1
+ import { getSupabaseClient } from '../../core/services/supabase';
2
+ import { subscribeManagedChannel } from '../../core/services/supabase/realtimeManager';
3
+ import type { AgentRun, AgentRunEvent } from './types';
4
+
5
+ type DbAgentRunRow = {
6
+ id: string;
7
+ app_id: string;
8
+ thread_id: string;
9
+ queue_item_id: string | null;
10
+ status: AgentRun['status'];
11
+ current_phase: AgentRun['currentPhase'];
12
+ last_seq: number;
13
+ summary: Record<string, unknown> | null;
14
+ error_code: string | null;
15
+ error_message: string | null;
16
+ started_at: string;
17
+ finished_at: string | null;
18
+ created_at: string;
19
+ updated_at: string;
20
+ };
21
+
22
+ type DbAgentRunEventRow = {
23
+ id: string;
24
+ run_id: string;
25
+ app_id: string;
26
+ thread_id: string;
27
+ queue_item_id: string | null;
28
+ seq: number;
29
+ event_type: AgentRunEvent['eventType'];
30
+ phase: AgentRunEvent['phase'];
31
+ tool_name: string | null;
32
+ path: string | null;
33
+ payload: Record<string, unknown> | null;
34
+ created_at: string;
35
+ };
36
+
37
+ function mapRun(row: DbAgentRunRow): AgentRun {
38
+ return {
39
+ id: row.id,
40
+ appId: row.app_id,
41
+ threadId: row.thread_id,
42
+ queueItemId: row.queue_item_id,
43
+ status: row.status,
44
+ currentPhase: row.current_phase,
45
+ lastSeq: Number(row.last_seq || 0),
46
+ summary: (row.summary || {}) as Record<string, unknown>,
47
+ errorCode: row.error_code,
48
+ errorMessage: row.error_message,
49
+ startedAt: row.started_at,
50
+ finishedAt: row.finished_at,
51
+ createdAt: row.created_at,
52
+ updatedAt: row.updated_at,
53
+ };
54
+ }
55
+
56
+ function mapEvent(row: DbAgentRunEventRow): AgentRunEvent {
57
+ return {
58
+ id: row.id,
59
+ runId: row.run_id,
60
+ appId: row.app_id,
61
+ threadId: row.thread_id,
62
+ queueItemId: row.queue_item_id,
63
+ seq: Number(row.seq || 0),
64
+ eventType: row.event_type,
65
+ phase: row.phase,
66
+ toolName: row.tool_name,
67
+ path: row.path,
68
+ payload: (row.payload || {}) as Record<string, unknown>,
69
+ createdAt: row.created_at,
70
+ };
71
+ }
72
+
73
+ export interface AgentProgressRepository {
74
+ getLatestRun(threadId: string): Promise<AgentRun | null>;
75
+ listEvents(runId: string, afterSeq?: number): Promise<AgentRunEvent[]>;
76
+ subscribeThreadRuns(
77
+ threadId: string,
78
+ handlers: {
79
+ onInsert?: (run: AgentRun) => void;
80
+ onUpdate?: (run: AgentRun) => void;
81
+ }
82
+ ): () => void;
83
+ subscribeRunEvents(
84
+ runId: string,
85
+ handlers: {
86
+ onInsert?: (event: AgentRunEvent) => void;
87
+ onUpdate?: (event: AgentRunEvent) => void;
88
+ }
89
+ ): () => void;
90
+ }
91
+
92
+ class AgentProgressRepositoryImpl implements AgentProgressRepository {
93
+ async getLatestRun(threadId: string): Promise<AgentRun | null> {
94
+ if (!threadId) return null;
95
+ const supabase = getSupabaseClient();
96
+ const { data, error } = await (supabase as any)
97
+ .from('agent_run')
98
+ .select('*')
99
+ .eq('thread_id', threadId)
100
+ .order('started_at', { ascending: false })
101
+ .limit(1)
102
+ .maybeSingle();
103
+ if (error) throw new Error(error.message || 'Failed to fetch latest agent run');
104
+ if (!data) return null;
105
+ return mapRun(data as DbAgentRunRow);
106
+ }
107
+
108
+ async listEvents(runId: string, afterSeq?: number): Promise<AgentRunEvent[]> {
109
+ if (!runId) return [];
110
+ const supabase = getSupabaseClient();
111
+ let query = (supabase as any).from('agent_run_event').select('*').eq('run_id', runId).order('seq', { ascending: true });
112
+ if (typeof afterSeq === 'number' && Number.isFinite(afterSeq) && afterSeq > 0) {
113
+ query = query.gt('seq', afterSeq);
114
+ }
115
+ const { data, error } = await query;
116
+ if (error) throw new Error(error.message || 'Failed to fetch agent run events');
117
+ const rows = Array.isArray(data) ? (data as DbAgentRunEventRow[]) : [];
118
+ return rows.map(mapEvent);
119
+ }
120
+
121
+ subscribeThreadRuns(
122
+ threadId: string,
123
+ handlers: {
124
+ onInsert?: (run: AgentRun) => void;
125
+ onUpdate?: (run: AgentRun) => void;
126
+ }
127
+ ): () => void {
128
+ return subscribeManagedChannel(`agent-progress:runs:thread:${threadId}`, (channel) => {
129
+ channel
130
+ .on(
131
+ 'postgres_changes',
132
+ { event: 'INSERT', schema: 'public', table: 'agent_run', filter: `thread_id=eq.${threadId}` },
133
+ (payload) => {
134
+ const row = payload.new as DbAgentRunRow;
135
+ handlers.onInsert?.(mapRun(row));
136
+ }
137
+ )
138
+ .on(
139
+ 'postgres_changes',
140
+ { event: 'UPDATE', schema: 'public', table: 'agent_run', filter: `thread_id=eq.${threadId}` },
141
+ (payload) => {
142
+ const row = payload.new as DbAgentRunRow;
143
+ handlers.onUpdate?.(mapRun(row));
144
+ }
145
+ );
146
+ });
147
+ }
148
+
149
+ subscribeRunEvents(
150
+ runId: string,
151
+ handlers: {
152
+ onInsert?: (event: AgentRunEvent) => void;
153
+ onUpdate?: (event: AgentRunEvent) => void;
154
+ }
155
+ ): () => void {
156
+ return subscribeManagedChannel(`agent-progress:events:run:${runId}`, (channel) => {
157
+ channel
158
+ .on(
159
+ 'postgres_changes',
160
+ { event: 'INSERT', schema: 'public', table: 'agent_run_event', filter: `run_id=eq.${runId}` },
161
+ (payload) => {
162
+ const row = payload.new as DbAgentRunEventRow;
163
+ handlers.onInsert?.(mapEvent(row));
164
+ }
165
+ )
166
+ .on(
167
+ 'postgres_changes',
168
+ { event: 'UPDATE', schema: 'public', table: 'agent_run_event', filter: `run_id=eq.${runId}` },
169
+ (payload) => {
170
+ const row = payload.new as DbAgentRunEventRow;
171
+ handlers.onUpdate?.(mapEvent(row));
172
+ }
173
+ );
174
+ });
175
+ }
176
+ }
177
+
178
+ export const agentProgressRepository: AgentProgressRepository = new AgentProgressRepositoryImpl();
179
+
@@ -0,0 +1,67 @@
1
+ export type AgentRunStatus = 'running' | 'succeeded' | 'failed' | 'cancelled';
2
+
3
+ export type AgentProgressPhase =
4
+ | 'planning'
5
+ | 'reasoning'
6
+ | 'analyzing'
7
+ | 'editing'
8
+ | 'executing'
9
+ | 'working'
10
+ | 'validating'
11
+ | 'finalizing';
12
+
13
+ export type AgentRunEventType =
14
+ | 'run_started'
15
+ | 'phase_changed'
16
+ | 'step_started'
17
+ | 'file_changed'
18
+ | 'todo_updated'
19
+ | 'run_completed'
20
+ | 'run_failed'
21
+ | 'run_cancelled';
22
+
23
+ export type AgentRun = {
24
+ id: string;
25
+ appId: string;
26
+ threadId: string;
27
+ queueItemId: string | null;
28
+ status: AgentRunStatus;
29
+ currentPhase: AgentProgressPhase | null;
30
+ lastSeq: number;
31
+ summary: Record<string, unknown>;
32
+ errorCode: string | null;
33
+ errorMessage: string | null;
34
+ startedAt: string;
35
+ finishedAt: string | null;
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ };
39
+
40
+ export type AgentRunEvent = {
41
+ id: string;
42
+ runId: string;
43
+ appId: string;
44
+ threadId: string;
45
+ queueItemId: string | null;
46
+ seq: number;
47
+ eventType: AgentRunEventType;
48
+ phase: AgentProgressPhase | null;
49
+ toolName: string | null;
50
+ path: string | null;
51
+ payload: Record<string, unknown>;
52
+ createdAt: string;
53
+ };
54
+
55
+ export type AgentTodoSummary = {
56
+ total: number;
57
+ pending: number;
58
+ inProgress: number;
59
+ completed: number;
60
+ currentTask: string | null;
61
+ };
62
+
63
+ export type AgentProgressSnapshot = {
64
+ run: AgentRun | null;
65
+ events: AgentRunEvent[];
66
+ };
67
+