@meistrari/tela-cli 0.7.0 → 0.7.4

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 (59) hide show
  1. package/README.md +56 -106
  2. package/dist/commands/auth.d.ts +2 -0
  3. package/dist/commands/context.d.ts +2 -0
  4. package/dist/commands/doctor-auth.d.ts +2 -0
  5. package/dist/commands/env.d.ts +2 -0
  6. package/dist/credentials.d.ts +2 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +7027 -0
  9. package/dist/output.d.ts +13 -0
  10. package/dist/program.d.ts +2 -0
  11. package/package.json +17 -24
  12. package/scripts/install-skills.cjs +127 -0
  13. package/skills/tela-cli/SKILL.md +20 -0
  14. package/skills/tela-cli/references/agent.md +111 -0
  15. package/cli.ts +0 -25
  16. package/postinstall.sh +0 -19
  17. package/src/agent-api.ts +0 -300
  18. package/src/assets.d.ts +0 -4
  19. package/src/auth.ts +0 -432
  20. package/src/claude.ts +0 -101
  21. package/src/commands/doctor.ts +0 -35
  22. package/src/commands/feedback.ts +0 -701
  23. package/src/commands/metadata.ts +0 -242
  24. package/src/commands/push.ts +0 -208
  25. package/src/commands/run.ts +0 -1699
  26. package/src/commands/status.ts +0 -109
  27. package/src/commands/sync.ts +0 -245
  28. package/src/commit-message.ts +0 -69
  29. package/src/config.ts +0 -799
  30. package/src/diagnostics.ts +0 -813
  31. package/src/errors.ts +0 -122
  32. package/src/forgejo.ts +0 -233
  33. package/src/git.ts +0 -457
  34. package/src/ink/assets/160092__jorickhoofd__dog-bark-1.mp3 +0 -0
  35. package/src/ink/bark.tsx +0 -241
  36. package/src/ink/components.tsx +0 -178
  37. package/src/ink/runtime.tsx +0 -90
  38. package/src/ink/screens/feedback-screen.tsx +0 -395
  39. package/src/ink/screens/push-screen.tsx +0 -232
  40. package/src/ink/screens/run-screen.tsx +0 -292
  41. package/src/ink/screens/setup/breadcrumb.tsx +0 -24
  42. package/src/ink/screens/setup/setup-screen.tsx +0 -612
  43. package/src/ink/screens/setup/step-frame.tsx +0 -31
  44. package/src/ink/screens/setup/steps/done-step.tsx +0 -29
  45. package/src/ink/screens/setup/steps/environment-step.tsx +0 -23
  46. package/src/ink/screens/setup/steps/organization-step.tsx +0 -35
  47. package/src/ink/screens/setup/steps/review-step.tsx +0 -78
  48. package/src/ink/screens/setup/steps/running-step.tsx +0 -37
  49. package/src/ink/screens/setup/steps/ssh-key-step.tsx +0 -48
  50. package/src/ink/screens/setup/steps/text-field-step.tsx +0 -18
  51. package/src/ink/screens/setup/steps.ts +0 -156
  52. package/src/ink/screens/setup/types.ts +0 -65
  53. package/src/ink/screens/sync-screen.tsx +0 -283
  54. package/src/process.ts +0 -251
  55. package/src/program.ts +0 -2105
  56. package/src/runtime.ts +0 -147
  57. package/src/types.ts +0 -165
  58. package/src/ui.ts +0 -295
  59. package/src/version.ts +0 -3
@@ -1,178 +0,0 @@
1
- /** @jsxImportSource react */
2
- import React, { useEffect, useMemo, useState, type ReactNode } from 'react'
3
- import { Box, Text } from 'ink'
4
- import { getTelaAsciiHeader } from '../ui.ts'
5
-
6
- export interface AppFrameProps {
7
- title: string
8
- subtitle?: string
9
- footer?: string
10
- status?: string
11
- mascotRows?: readonly string[]
12
- children: ReactNode
13
- }
14
-
15
- export function AppFrame(props: AppFrameProps): React.ReactElement {
16
- const mascot = props.mascotRows
17
- const header = useMemo(() => getTelaAsciiHeader(), [])
18
-
19
- return (
20
- <Box flexDirection="column" paddingX={2} paddingY={1}>
21
- <Box flexDirection="column">
22
- {header.map((line, index) => (
23
- <Text key={`header-${index}`} color="white">
24
- {line}
25
- </Text>
26
- ))}
27
- </Box>
28
- <Box flexDirection="column" marginTop={1}>
29
- <Text color="white" bold>Tela CLI · {props.title}</Text>
30
- {props.subtitle ? <Text dimColor>{props.subtitle}</Text> : null}
31
- {props.status ? <Text color="yellow">{props.status}</Text> : null}
32
- </Box>
33
- {mascot ? (
34
- <Box flexDirection="column" marginTop={1}>
35
- {mascot.map((row, index) => (
36
- <Box key={`mascot-${index}`} flexDirection="row">
37
- {row.split('').map((cell, cellIndex) => {
38
- const level = Number.parseInt(cell, 16)
39
- const channel = Math.round((level / 15) * 255)
40
- const colorValue = `#${channel.toString(16).padStart(2, '0').repeat(3)}`
41
- return (
42
- <Text key={`pixel-${index}-${cellIndex}`} backgroundColor={colorValue}>
43
- {' '}
44
- </Text>
45
- )
46
- })}
47
- </Box>
48
- ))}
49
- </Box>
50
- ) : null}
51
- <Box flexDirection="column" marginTop={1}>
52
- {props.children}
53
- </Box>
54
- {props.footer ? (
55
- <Box marginTop={1}>
56
- <Text dimColor>{props.footer}</Text>
57
- </Box>
58
- ) : null}
59
- </Box>
60
- )
61
- }
62
-
63
- export function Section(props: {
64
- title: string
65
- children: ReactNode
66
- }): React.ReactElement {
67
- return (
68
- <Box flexDirection="column" marginTop={1}>
69
- <Text color="cyan">{props.title}</Text>
70
- <Box flexDirection="column" marginLeft={2}>
71
- {props.children}
72
- </Box>
73
- </Box>
74
- )
75
- }
76
-
77
- export function KeyHints(props: {
78
- items: string[]
79
- }): React.ReactElement {
80
- return (
81
- <Text dimColor>{props.items.join(' ')}</Text>
82
- )
83
- }
84
-
85
- export function SelectList<T>(props: {
86
- items: Array<{
87
- key: T
88
- label: string
89
- hint?: string
90
- selected?: boolean
91
- }>
92
- currentIndex: number
93
- multiselect?: boolean
94
- }): React.ReactElement {
95
- return (
96
- <Box flexDirection="column">
97
- {props.items.map((item, index) => {
98
- const focused = index === props.currentIndex
99
- const prefix = focused ? '›' : ' '
100
- const selected = props.multiselect
101
- ? (item.selected ? '[x]' : '[ ]')
102
- : ''
103
-
104
- return (
105
- <Text key={String(index)} color={focused ? 'greenBright' : undefined}>
106
- {`${prefix} ${selected} ${item.label}`.trimEnd()}
107
- {item.hint ? <Text dimColor>{` — ${item.hint}`}</Text> : null}
108
- </Text>
109
- )
110
- })}
111
- </Box>
112
- )
113
- }
114
-
115
- export function TextEntry(props: {
116
- label: string
117
- value: string
118
- placeholder?: string
119
- error?: string
120
- masked?: boolean
121
- }): React.ReactElement {
122
- const displayValue = props.value.length > 0
123
- ? (props.masked ? '*'.repeat(props.value.length) : props.value)
124
- : (props.placeholder ? `<${props.placeholder}>` : '')
125
-
126
- return (
127
- <Box flexDirection="column">
128
- <Text>{props.label}</Text>
129
- <Text color="greenBright">{displayValue || ' '}</Text>
130
- {props.error ? <Text color="red">{props.error}</Text> : null}
131
- </Box>
132
- )
133
- }
134
-
135
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
136
-
137
- export function SpinnerText(props: {
138
- label: string
139
- active?: boolean
140
- }): React.ReactElement {
141
- const [frameIndex, setFrameIndex] = useState(0)
142
-
143
- useEffect(() => {
144
- if (props.active === false) {
145
- return
146
- }
147
-
148
- const interval = setInterval(() => {
149
- setFrameIndex(current => (current + 1) % SPINNER_FRAMES.length)
150
- }, 80)
151
-
152
- return () => {
153
- clearInterval(interval)
154
- }
155
- }, [props.active])
156
-
157
- return (
158
- <Text color="yellow">
159
- {`${SPINNER_FRAMES[frameIndex]} ${props.label}`}
160
- </Text>
161
- )
162
- }
163
-
164
- export function Callout(props: {
165
- tone: 'info' | 'warn' | 'error' | 'success'
166
- children: ReactNode
167
- }): React.ReactElement {
168
- const color = {
169
- info: 'blueBright',
170
- warn: 'yellowBright',
171
- error: 'redBright',
172
- success: 'greenBright',
173
- } as const
174
-
175
- return (
176
- <Text color={color[props.tone]}>{props.children}</Text>
177
- )
178
- }
@@ -1,90 +0,0 @@
1
- /** @jsxImportSource react */
2
- import React from 'react'
3
- import { render, type Instance, type RenderOptions } from 'ink'
4
-
5
- export interface CommandScreenController<T> {
6
- resolve(value: T, delayMs?: number): void
7
- reject(error: unknown, delayMs?: number): void
8
- }
9
-
10
- export interface RenderCommandScreenOptions extends RenderOptions {
11
- /**
12
- * When true, the Ink instance erases its rendered output before unmounting
13
- * so the final frame is not persisted to the terminal scrollback. Useful
14
- * for transient screens like the bark intro that hand off to the next
15
- * screen without leaving a visual residue.
16
- */
17
- clearOnExit?: boolean
18
- }
19
-
20
- export function renderCommandScreen<T>(
21
- build: (controller: CommandScreenController<T>) => React.ReactElement,
22
- options: RenderCommandScreenOptions = {},
23
- ): Promise<T> {
24
- const { clearOnExit, ...renderOptions } = options
25
-
26
- return new Promise<T>((resolve, reject) => {
27
- let settled = false
28
- let instance: Instance | undefined
29
-
30
- const teardown = () => {
31
- if (clearOnExit) {
32
- instance?.clear()
33
- }
34
- instance?.unmount()
35
- }
36
-
37
- const settle = (
38
- handler: (value: T) => void | never,
39
- payload: T,
40
- delayMs = 0,
41
- ) => {
42
- if (settled) {
43
- return
44
- }
45
-
46
- settled = true
47
- const finish = () => {
48
- teardown()
49
- handler(payload)
50
- }
51
-
52
- if (delayMs > 0) {
53
- setTimeout(finish, delayMs)
54
- return
55
- }
56
-
57
- queueMicrotask(finish)
58
- }
59
-
60
- const controller: CommandScreenController<T> = {
61
- resolve(value, delayMs) {
62
- settle(resolve, value, delayMs)
63
- },
64
- reject(error, delayMs) {
65
- if (settled) {
66
- return
67
- }
68
-
69
- settled = true
70
- const finish = () => {
71
- teardown()
72
- reject(error)
73
- }
74
-
75
- if (delayMs && delayMs > 0) {
76
- setTimeout(finish, delayMs)
77
- return
78
- }
79
-
80
- queueMicrotask(finish)
81
- },
82
- }
83
-
84
- instance = render(build(controller), {
85
- exitOnCtrlC: true,
86
- patchConsole: false,
87
- ...renderOptions,
88
- })
89
- })
90
- }
@@ -1,395 +0,0 @@
1
- /** @jsxImportSource react */
2
- import React, { useEffect, useMemo, useState } from 'react'
3
- import { Box, Text, useInput } from 'ink'
4
- import type {
5
- FeedbackData,
6
- FeedbackEmptyState,
7
- FeedbackEntry,
8
- FeedbackSelectionMode,
9
- } from '../../commands/feedback.ts'
10
- import {
11
- buildClaudeFeedbackPrompt,
12
- buildFeedbackNextActions,
13
- buildFeedbackReviewModel,
14
- describeFeedbackEmptyState,
15
- describeFeedbackSelectionMode,
16
- formatFeedbackSelectionSummary,
17
- getFeedbackEntryTitle,
18
- } from '../../commands/feedback.ts'
19
- import { AppFrame, Callout, KeyHints, Section } from '../components.tsx'
20
- import type { CommandScreenController } from '../runtime.tsx'
21
-
22
- export interface FeedbackSelectionResult {
23
- selectedFeedbacks: FeedbackEntry[]
24
- prompt?: string
25
- launchedClaude: boolean
26
- selectionMode: FeedbackSelectionMode
27
- }
28
-
29
- export interface FeedbackSelectionScreenProps {
30
- controller: CommandScreenController<FeedbackSelectionResult>
31
- data: FeedbackData
32
- mode: FeedbackSelectionMode
33
- }
34
-
35
- type FeedbackPhase = 'browse' | 'confirm'
36
-
37
- function renderCatalog(
38
- entries: FeedbackEntry[],
39
- input: {
40
- currentEntryId?: string
41
- selectedIds: Set<string>
42
- selectionEditable: boolean
43
- selectionVisible: boolean
44
- },
45
- ): React.ReactElement {
46
- const actionable = entries.filter(entry => entry.actionable)
47
- const informational = entries.filter(entry => !entry.actionable)
48
-
49
- const renderEntry = (entry: FeedbackEntry): React.ReactElement => {
50
- const focused = entry.id === input.currentEntryId
51
- const marker = focused ? '›' : ' '
52
- const checkbox = input.selectionVisible
53
- ? entry.actionable
54
- ? input.selectedIds.has(entry.id) ? '[x]' : '[ ]'
55
- : '[-]'
56
- : ''
57
-
58
- return (
59
- <Text key={entry.id} color={focused ? 'greenBright' : undefined}>
60
- {`${marker} ${checkbox} #${entry.number} [${entry.feedback.status}] ${getFeedbackEntryTitle(entry)}`.trimEnd()}
61
- <Text dimColor>{` — ${entry.feedback.reason || '<empty>'}`}</Text>
62
- </Text>
63
- )
64
- }
65
-
66
- return (
67
- <Box flexDirection="column">
68
- {actionable.length > 0 ? (
69
- <>
70
- <Text color="cyan">Actionable</Text>
71
- {actionable.map(renderEntry)}
72
- </>
73
- ) : null}
74
- {informational.length > 0 ? (
75
- <>
76
- {actionable.length > 0 ? <Text> </Text> : null}
77
- <Text color="cyan">Informational</Text>
78
- {informational.map(renderEntry)}
79
- </>
80
- ) : null}
81
- {entries.length === 0 ? <Text dimColor>No feedback entries found for this commit.</Text> : null}
82
- {!input.selectionEditable && input.selectionVisible ? (
83
- <Text dimColor>Selection is locked for this review mode.</Text>
84
- ) : null}
85
- </Box>
86
- )
87
- }
88
-
89
- function buildBrowseFooter(input: {
90
- mode: FeedbackSelectionMode
91
- selectionEditable: boolean
92
- }): string {
93
- if (input.mode === 'list') {
94
- return '↑/↓ move · Enter finish review · Esc exit'
95
- }
96
-
97
- if (input.selectionEditable) {
98
- return '↑/↓ move · Space/x toggle · a select-all · n clear · Enter continue/finish · Esc exit'
99
- }
100
-
101
- return '↑/↓ move · Enter continue · Esc exit'
102
- }
103
-
104
- function formatSelectedTitles(entries: FeedbackEntry[]): string[] {
105
- return entries.map(entry => `#${entry.number} ${getFeedbackEntryTitle(entry)}`)
106
- }
107
-
108
- function emptyStateTone(emptyState: FeedbackEmptyState): 'info' | 'warn' {
109
- return emptyState === 'none' ? 'warn' : 'info'
110
- }
111
-
112
- export function FeedbackSelectionScreen(props: FeedbackSelectionScreenProps): React.ReactElement {
113
- const review = useMemo(() => buildFeedbackReviewModel(props.data), [props.data])
114
- const [phase, setPhase] = useState<FeedbackPhase>('browse')
115
- const [cursor, setCursor] = useState(0)
116
- const [selectedIds, setSelectedIds] = useState<Set<string>>(() => {
117
- if (props.mode === 'all' || props.mode === 'default') {
118
- return new Set(props.data.actionableEntries.map(entry => entry.id))
119
- }
120
-
121
- return new Set<string>()
122
- })
123
-
124
- const currentEntry = props.data.entries[cursor] ?? props.data.entries[0]
125
- const selectionEditable = props.mode === 'select' || props.mode === 'default'
126
- const selectionVisible = props.mode !== 'list' && review.emptyState === 'none'
127
- const selectedFeedbacks = useMemo(() => {
128
- return props.data.actionableEntries.filter(entry => selectedIds.has(entry.id))
129
- }, [props.data.actionableEntries, selectedIds])
130
- const prompt = selectedFeedbacks.length > 0
131
- ? buildClaudeFeedbackPrompt({
132
- repoRoot: props.data.repoRoot,
133
- organizationName: props.data.organizationName,
134
- repository: props.data.repository,
135
- commitHash: props.data.commitHash,
136
- selectedFeedbacks,
137
- })
138
- : undefined
139
-
140
- useInput((input, key) => {
141
- if (phase === 'browse') {
142
- if ((key.downArrow || input === 'j') && props.data.entries.length > 0) {
143
- setCursor(current => Math.min(current + 1, props.data.entries.length - 1))
144
- return
145
- }
146
-
147
- if ((key.upArrow || input === 'k') && props.data.entries.length > 0) {
148
- setCursor(current => Math.max(current - 1, 0))
149
- return
150
- }
151
-
152
- if (key.escape) {
153
- props.controller.resolve({
154
- selectedFeedbacks: [],
155
- launchedClaude: false,
156
- selectionMode: props.mode,
157
- })
158
- return
159
- }
160
-
161
- if (selectionEditable && selectionVisible && (input === ' ' || input === 'x')) {
162
- if (!currentEntry?.actionable) {
163
- return
164
- }
165
-
166
- setSelectedIds(current => {
167
- const next = new Set(current)
168
- if (next.has(currentEntry.id)) {
169
- next.delete(currentEntry.id)
170
- }
171
- else {
172
- next.add(currentEntry.id)
173
- }
174
- return next
175
- })
176
- return
177
- }
178
-
179
- if (selectionEditable && selectionVisible && input === 'a') {
180
- setSelectedIds(new Set(props.data.actionableEntries.map(entry => entry.id)))
181
- return
182
- }
183
-
184
- if (selectionEditable && selectionVisible && input === 'n') {
185
- setSelectedIds(new Set())
186
- return
187
- }
188
-
189
- if (key.return) {
190
- if (props.mode === 'list' || review.emptyState !== 'none' || selectedFeedbacks.length === 0) {
191
- props.controller.resolve({
192
- selectedFeedbacks,
193
- launchedClaude: false,
194
- selectionMode: props.mode,
195
- })
196
- return
197
- }
198
-
199
- setPhase('confirm')
200
- }
201
- return
202
- }
203
-
204
- if (phase === 'confirm') {
205
- if (key.escape) {
206
- setPhase('browse')
207
- return
208
- }
209
-
210
- if (key.return) {
211
- props.controller.resolve({
212
- selectedFeedbacks,
213
- prompt,
214
- launchedClaude: false,
215
- selectionMode: props.mode,
216
- })
217
- }
218
- }
219
- })
220
-
221
- return (
222
- <AppFrame
223
- title="Feedback"
224
- subtitle="Review commit feedback, refine the selection, and preview the exact Claude task."
225
- footer={phase === 'browse' ? buildBrowseFooter({ mode: props.mode, selectionEditable }) : 'Enter launch Claude · Esc back'}
226
- >
227
- {phase === 'browse' ? (
228
- <Box flexDirection="row" gap={2}>
229
- <Box flexDirection="column" width={56}>
230
- <Section title="Catalog">
231
- {renderCatalog(props.data.entries, {
232
- currentEntryId: currentEntry?.id,
233
- selectedIds,
234
- selectionEditable,
235
- selectionVisible,
236
- })}
237
- </Section>
238
- </Box>
239
- <Box flexDirection="column" flexGrow={1}>
240
- <Section title="Review summary">
241
- <Text>Repository: {props.data.organizationName}/{props.data.repository}</Text>
242
- <Text>Commit: {props.data.commitHash}</Text>
243
- <Text>Mode: {describeFeedbackSelectionMode(props.mode)}</Text>
244
- <Text>Feedbacks: {review.summary.totalFeedbacks}</Text>
245
- <Text>Actionable: {review.summary.actionableCount}</Text>
246
- <Text>Informational: {review.summary.informationalCount}</Text>
247
- </Section>
248
-
249
- <Section title="Detail">
250
- {currentEntry ? (
251
- <>
252
- <Text>Focus: #{currentEntry.number}</Text>
253
- <Text>Status: {currentEntry.feedback.status}</Text>
254
- <Text>Reason: {currentEntry.feedback.reason || '<empty>'}</Text>
255
- <Text>Suggestion: {currentEntry.feedback.suggestion.trim() || '<none>'}</Text>
256
- <Text>Actionable: {currentEntry.actionable ? 'yes' : 'no'}</Text>
257
- </>
258
- ) : (
259
- <Text>No feedback entries found.</Text>
260
- )}
261
- </Section>
262
-
263
- <Section title="Selection">
264
- <Text>{`Selected actionable suggestions: ${selectedFeedbacks.length}`}</Text>
265
- <Text>{`Selection mode: ${describeFeedbackSelectionMode(props.mode)}`}</Text>
266
- </Section>
267
-
268
- {review.emptyState !== 'none' ? (
269
- <Callout tone={emptyStateTone(review.emptyState)}>
270
- {`${describeFeedbackEmptyState(review.emptyState)} Press Enter to finish the review.`}
271
- </Callout>
272
- ) : null}
273
- {review.emptyState === 'none' && selectionVisible && selectedFeedbacks.length === 0 ? (
274
- <Callout tone="warn">
275
- No suggestions are selected. Press Enter to finish without launching Claude, or choose suggestions first.
276
- </Callout>
277
- ) : null}
278
- </Box>
279
- </Box>
280
- ) : null}
281
-
282
- {phase === 'confirm' ? (
283
- <>
284
- <Section title="Selected suggestions">
285
- {formatSelectedTitles(selectedFeedbacks).map(line => (
286
- <Text key={line}>{line}</Text>
287
- ))}
288
- </Section>
289
- <Section title="Selection summary">
290
- {formatFeedbackSelectionSummary({
291
- selectionMode: props.mode,
292
- selectedFeedbacks,
293
- }).split('\n').map((line, index) => (
294
- <Text key={`selection-${index}`}>{line}</Text>
295
- ))}
296
- </Section>
297
- <Section title="Claude task">
298
- {prompt?.split('\n').map((line, index) => (
299
- <Text key={`prompt-${index}`}>{line}</Text>
300
- ))}
301
- </Section>
302
- <Callout tone="info">
303
- {`Claude will receive ${selectedFeedbacks.length} selected suggestion${selectedFeedbacks.length === 1 ? '' : 's'}.`}
304
- </Callout>
305
- </>
306
- ) : null}
307
-
308
- {phase === 'browse' ? (
309
- <KeyHints items={[
310
- `Commit ${props.data.commitHash}`,
311
- `${review.summary.totalFeedbacks} feedback item${review.summary.totalFeedbacks === 1 ? '' : 's'}`,
312
- props.mode === 'list' ? 'Review only' : `${selectedFeedbacks.length} selected`,
313
- ]} />
314
- ) : null}
315
- </AppFrame>
316
- )
317
- }
318
-
319
- export function FeedbackClaudeHandoffScreen(props: {
320
- controller: CommandScreenController<void>
321
- repoRoot: string
322
- commitHash: string
323
- selectedFeedbacks: FeedbackEntry[]
324
- launchDelayMs?: number
325
- }): React.ReactElement {
326
- useEffect(() => {
327
- props.controller.resolve(undefined, props.launchDelayMs ?? 650)
328
- }, [props.controller, props.launchDelayMs])
329
-
330
- return (
331
- <AppFrame
332
- title="Feedback"
333
- subtitle="Handing off to native Claude. Tela will resume after Claude exits."
334
- status="Launching Claude review…"
335
- >
336
- <Section title="Overview">
337
- <Text>Repo root: {props.repoRoot}</Text>
338
- <Text>Commit: {props.commitHash}</Text>
339
- <Text>Selected suggestions: {props.selectedFeedbacks.length}</Text>
340
- </Section>
341
- <Section title="Selected suggestions">
342
- {formatSelectedTitles(props.selectedFeedbacks).map(line => (
343
- <Text key={line}>{line}</Text>
344
- ))}
345
- </Section>
346
- <Callout tone="info">
347
- Claude will take over this terminal to apply the selected feedback. Exit Claude to return to Tela for the outcome review.
348
- </Callout>
349
- </AppFrame>
350
- )
351
- }
352
-
353
- export function FeedbackOutcomeScreen(props: {
354
- controller: CommandScreenController<void>
355
- commitHash: string
356
- selectionMode: FeedbackSelectionMode
357
- diffStat: string
358
- selectedFeedbacks: FeedbackEntry[]
359
- }): React.ReactElement {
360
- useEffect(() => {
361
- props.controller.resolve(undefined, 40)
362
- }, [props.controller])
363
-
364
- return (
365
- <AppFrame
366
- title="Feedback"
367
- subtitle="Claude finished applying the selected feedback suggestions."
368
- >
369
- <Section title="Summary">
370
- <Text>Commit: {props.commitHash}</Text>
371
- <Text>Mode: {describeFeedbackSelectionMode(props.selectionMode)}</Text>
372
- <Text>Selected suggestions: {props.selectedFeedbacks.length}</Text>
373
- </Section>
374
- <Section title="Selected suggestions">
375
- {formatSelectedTitles(props.selectedFeedbacks).map(line => (
376
- <Text key={line}>{line}</Text>
377
- ))}
378
- </Section>
379
- <Section title="Local diff summary">
380
- {props.diffStat.split('\n').map((line, index) => (
381
- <Text key={`diff-${index}`}>{line}</Text>
382
- ))}
383
- </Section>
384
- <Section title="Next actions">
385
- {buildFeedbackNextActions({
386
- launchedClaude: true,
387
- actionableCount: props.selectedFeedbacks.length,
388
- emptyState: 'none',
389
- }).map(action => (
390
- <Text key={action}>{action}</Text>
391
- ))}
392
- </Section>
393
- </AppFrame>
394
- )
395
- }