@alecsibilia/luca 14.0.0-alpha.4 → 14.0.0-alpha.6
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/CHANGELOG.md +8 -0
- package/README.md +5 -2
- package/board/client/board-panel.tsx +44 -70
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-server.ts +65 -1
- package/board/server/board-vocabulary.ts +18 -5
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +8 -0
- package/board/server/reduce-board.ts +43 -14
- package/board/server/reply-post.ts +407 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state.ts +8 -0
- package/board/shared/reply-actions.ts +201 -0
- package/engine/agents/role-prompts.ts +22 -0
- package/engine/cli/computer-adapters-real.ts +14 -2
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +51 -3
- package/engine/cli/doctor.ts +14 -2
- package/engine/cli/init.ts +17 -1
- package/engine/cli/luca-skills.ts +159 -0
- package/engine/cli/upgrade-command.ts +1 -0
- package/engine/cli/upgrade.ts +23 -6
- package/engine/core/decide-build.ts +27 -12
- package/engine/core/decide-run-budget.ts +12 -3
- package/engine/core/execute-build.ts +95 -41
- package/engine/core/execute-changeset.ts +6 -8
- package/engine/core/execute-final-review.ts +86 -10
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-stuck.ts +7 -9
- package/engine/core/execute.ts +27 -2
- package/engine/core/final-review-text.ts +13 -4
- package/engine/core/pull-request-text.ts +140 -0
- package/engine/core/stuck-text.ts +29 -0
- package/engine/gates/leftover-scan.ts +58 -4
- package/engine/git/git-adapter.ts +29 -0
- package/engine/journal/journal-record.ts +37 -5
- package/engine/journal/replay.ts +27 -10
- package/engine/tracker/github-tracker.ts +19 -9
- package/engine/tracker/in-memory-tracker.ts +3 -2
- package/engine/tracker/tracker.ts +37 -4
- package/package.json +2 -1
- package/skills/luca-unstick/SKILL.md +149 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1005 -0
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -38,14 +38,16 @@ This is Luca v14, in alpha. Old Luca (v13) is still on npm; see [Coming from v13
|
|
|
38
38
|
5. Write a spec with `/to-spec`, then split it into tickets with `/to-tickets`.
|
|
39
39
|
6. In a Paseo chat, type `/luca-run <spec number>`. The board shows the run in the side panel, and the run ends in one pull request.
|
|
40
40
|
|
|
41
|
+
If a ticket gets stuck, Luca comments on the spec and waits for your one-word reply. Type `/luca-unstick` in Claude Code or a Paseo chat, in the repo: it reads why, says what to do, and posts the reply once you say yes.
|
|
42
|
+
|
|
41
43
|
## Commands
|
|
42
44
|
|
|
43
45
|
| Command | What it does |
|
|
44
46
|
| ------- | ------------ |
|
|
45
|
-
| `luca init` | Sets up this computer, once (safe to run again): MuninnDB and its Claude Code entry, the board in Paseo with its engine and Bun paths,
|
|
47
|
+
| `luca init` | Sets up this computer, once (safe to run again): MuninnDB and its Claude Code entry, the board in Paseo with its engine and Bun paths, the planning skills (`to-spec`, `to-tickets`, `setup-matt-pocock-skills`, `grilling`, `domain-modeling`), and Luca's own `/luca-unstick` skill. It asks before turning on Paseo's plugins, and leaves skills you already have alone (it only replaces its own `/luca-unstick`). `--skip-skills` skips the planning skills. Inside a git repo, it then runs `luca setup` for that repo. |
|
|
46
48
|
| `luca setup` | Gets the repo in this folder ready: the labels a run needs (and the `release:*` labels when the repo uses changesets), `.luca/config.json` (a new one gets the repo's GitHub name as its vault; an existing vault is kept), and a check of GitHub sub-issues and dependencies. `--base <branch>` names the base branch. |
|
|
47
49
|
| `luca doctor` | Checks this computer, this repo when you're in one, and what v13 left behind. Each problem comes with its exact fix. `luca doctor --fix` fixes what's safe without asking: it never deletes (v13 files go to a dated backup folder) and never commits, and it lists the repo files to commit. |
|
|
48
|
-
| `luca upgrade` | Moves to the newest Luca on your channel: `alpha` stays on `alpha`, and it never goes back to v13. It refuses while any run is going (stuck runs and limit waits count) and lists them. A run that crashed, with no engine running it, doesn't stop it: upgrade says how to resume it on the new version. Then it reloads the board and says when to run `/reload-skills`. |
|
|
50
|
+
| `luca upgrade` | Moves to the newest Luca on your channel: `alpha` stays on `alpha`, and it never goes back to v13. It refuses while any run is going (stuck runs and limit waits count) and lists them. A run that crashed, with no engine running it, doesn't stop it: upgrade says how to resume it on the new version. Then it reloads the board, copies the new `/luca-unstick` skill, and says when to run `/reload-skills`. |
|
|
49
51
|
| `luca upgrade --to <version>` | Installs that exact version, an older one too, to go back when a version breaks. |
|
|
50
52
|
|
|
51
53
|
`init`, `setup`, and `upgrade` end with their part of `luca doctor`'s checks.
|
|
@@ -88,6 +90,7 @@ This is the one package that goes to npm. It's TypeScript source with no build a
|
|
|
88
90
|
| ------ | ---------- |
|
|
89
91
|
| `engine/` | The engine's source, from [`packages/engine/src`](../engine/src). Both bins point into it. |
|
|
90
92
|
| `board/` | The board plugin's folder, from [`packages/board`](../board): `paseo-plugin.json`, its entry points, its client, server, and shared code, and its own `package.json`, so Paseo can install it as a folder source. |
|
|
93
|
+
| `skills/` | Luca's own Claude Code skills, such as `luca-unstick`, from [`packages/engine/skills`](../engine/skills). `luca init` and `luca upgrade` copy them to `~/.claude/skills`. |
|
|
91
94
|
| `LICENSE` | The repo's license. |
|
|
92
95
|
|
|
93
96
|
Test files and test helpers stay out. The engine and the board stay private workspace packages, and this package lists every package they import at runtime as its own dependency.
|
|
@@ -6,12 +6,7 @@ import {
|
|
|
6
6
|
useWorkspace,
|
|
7
7
|
type PluginWorkspacePanelProps,
|
|
8
8
|
} from '@getpaseo/plugin/client'
|
|
9
|
-
import {
|
|
10
|
-
Icon,
|
|
11
|
-
ScrollView,
|
|
12
|
-
copyText,
|
|
13
|
-
useToast,
|
|
14
|
-
} from '@getpaseo/plugin/client/react-native'
|
|
9
|
+
import { Icon, ScrollView } from '@getpaseo/plugin/client/react-native'
|
|
15
10
|
import { ExternalLink } from '@getpaseo/plugin/client/ui'
|
|
16
11
|
import { useQuery } from '@tanstack/react-query'
|
|
17
12
|
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
|
@@ -31,6 +26,7 @@ import {
|
|
|
31
26
|
usageLineWaitText,
|
|
32
27
|
useNow,
|
|
33
28
|
} from './board-look'
|
|
29
|
+
import { StuckActions } from './stuck-actions'
|
|
34
30
|
|
|
35
31
|
import { boardReadRpc } from '../shared/board-rpc'
|
|
36
32
|
import {
|
|
@@ -54,6 +50,7 @@ import {
|
|
|
54
50
|
type TicketCard,
|
|
55
51
|
type TicketStage,
|
|
56
52
|
} from '../shared/board-state'
|
|
53
|
+
import { postedText, type PostedReply } from '../shared/reply-actions'
|
|
57
54
|
|
|
58
55
|
/**
|
|
59
56
|
* The side panel: one run of this workspace, top to bottom. Plan usage, a
|
|
@@ -254,21 +251,6 @@ const makeStyles = ({
|
|
|
254
251
|
fontSize: 12,
|
|
255
252
|
fontWeight: '700',
|
|
256
253
|
},
|
|
257
|
-
replyRow: {
|
|
258
|
-
flexDirection: 'row',
|
|
259
|
-
flexWrap: 'wrap',
|
|
260
|
-
alignItems: 'center',
|
|
261
|
-
gap: 5,
|
|
262
|
-
},
|
|
263
|
-
reply: {
|
|
264
|
-
color: colors.foreground,
|
|
265
|
-
backgroundColor: colors.surface2,
|
|
266
|
-
borderRadius: 4,
|
|
267
|
-
paddingHorizontal: 6,
|
|
268
|
-
paddingVertical: 2,
|
|
269
|
-
fontSize: 12,
|
|
270
|
-
fontFamily: MONO,
|
|
271
|
-
},
|
|
272
254
|
rule: { height: 1, backgroundColor: colors.border, marginVertical: 4 },
|
|
273
255
|
reviewTitle: {
|
|
274
256
|
color: colors.foreground,
|
|
@@ -439,56 +421,21 @@ const StepLine = ({
|
|
|
439
421
|
)
|
|
440
422
|
}
|
|
441
423
|
|
|
442
|
-
/** The exact replies; tap one to copy it. */
|
|
443
|
-
const Replies = ({
|
|
444
|
-
replies,
|
|
445
|
-
spec_number,
|
|
446
|
-
styles,
|
|
447
|
-
}: {
|
|
448
|
-
replies: string[]
|
|
449
|
-
spec_number: number | null
|
|
450
|
-
styles: Styles
|
|
451
|
-
}) => {
|
|
452
|
-
const toast = useToast()
|
|
453
|
-
const copy = async ({ reply }: { reply: string }) => {
|
|
454
|
-
try {
|
|
455
|
-
await copyText(reply)
|
|
456
|
-
toast.show(`Copied "${reply}"`, { variant: 'success' })
|
|
457
|
-
} catch {
|
|
458
|
-
toast.error('Could not copy. Select the text and use Copy.')
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
return (
|
|
462
|
-
<View style={styles.replyRow}>
|
|
463
|
-
<Text style={styles.label}>
|
|
464
|
-
Reply on{' '}
|
|
465
|
-
{spec_number === null
|
|
466
|
-
? 'the spec issue'
|
|
467
|
-
: `spec issue #${spec_number}`}{' '}
|
|
468
|
-
with
|
|
469
|
-
</Text>
|
|
470
|
-
{replies.map((reply) => (
|
|
471
|
-
<Pressable
|
|
472
|
-
key={reply}
|
|
473
|
-
accessibilityRole="button"
|
|
474
|
-
accessibilityLabel={`Copy the reply ${reply}`}
|
|
475
|
-
onPress={() => void copy({ reply })}
|
|
476
|
-
>
|
|
477
|
-
<Text style={styles.reply} selectable>
|
|
478
|
-
{reply}
|
|
479
|
-
</Text>
|
|
480
|
-
</Pressable>
|
|
481
|
-
))}
|
|
482
|
-
</View>
|
|
483
|
-
)
|
|
484
|
-
}
|
|
485
|
-
|
|
486
424
|
const NeedsYouCard = ({
|
|
487
425
|
item,
|
|
426
|
+
run_id,
|
|
488
427
|
spec_number,
|
|
428
|
+
posted,
|
|
429
|
+
chat_agent_id,
|
|
489
430
|
theme,
|
|
490
431
|
styles,
|
|
491
|
-
}: {
|
|
432
|
+
}: {
|
|
433
|
+
item: NeedsYou
|
|
434
|
+
run_id: string
|
|
435
|
+
spec_number: number | null
|
|
436
|
+
posted: PostedReply[]
|
|
437
|
+
chat_agent_id: string | null
|
|
438
|
+
} & Look) => (
|
|
492
439
|
<View style={[styles.card, styles.cardStuck]}>
|
|
493
440
|
<View style={styles.cardTop}>
|
|
494
441
|
<Icon
|
|
@@ -518,10 +465,13 @@ const NeedsYouCard = ({
|
|
|
518
465
|
))}
|
|
519
466
|
</>
|
|
520
467
|
) : null}
|
|
521
|
-
<
|
|
522
|
-
|
|
468
|
+
<StuckActions
|
|
469
|
+
run_id={run_id}
|
|
470
|
+
item={item}
|
|
523
471
|
spec_number={spec_number}
|
|
524
|
-
|
|
472
|
+
posted={posted}
|
|
473
|
+
chat_agent_id={chat_agent_id}
|
|
474
|
+
theme={theme}
|
|
525
475
|
/>
|
|
526
476
|
</View>
|
|
527
477
|
)
|
|
@@ -1096,6 +1046,15 @@ export const BoardPanel = ({
|
|
|
1096
1046
|
|
|
1097
1047
|
const { run } = state
|
|
1098
1048
|
const runs = board.data?.runs ?? []
|
|
1049
|
+
const posted = board.data?.posted ?? []
|
|
1050
|
+
const chat_agent_id = board.data?.chat_agent_id ?? null
|
|
1051
|
+
// Replies the board posted whose stuck item is gone: say what came of them.
|
|
1052
|
+
const answered = posted.filter(
|
|
1053
|
+
(entry) =>
|
|
1054
|
+
!state.needs_you.some(
|
|
1055
|
+
(item) => item.key === entry.key && item.since === entry.since
|
|
1056
|
+
)
|
|
1057
|
+
)
|
|
1099
1058
|
const card = (ticket: TicketCard) => {
|
|
1100
1059
|
const key = `ticket-${ticket.number}`
|
|
1101
1060
|
return (
|
|
@@ -1257,14 +1216,29 @@ export const BoardPanel = ({
|
|
|
1257
1216
|
<View style={styles.stageBody}>
|
|
1258
1217
|
{state.needs_you.map((item) => (
|
|
1259
1218
|
<NeedsYouCard
|
|
1260
|
-
key={item.key}
|
|
1219
|
+
key={`${item.key}-${item.since}`}
|
|
1261
1220
|
item={item}
|
|
1221
|
+
run_id={run.run_id}
|
|
1262
1222
|
spec_number={run.spec_number}
|
|
1223
|
+
posted={posted}
|
|
1224
|
+
chat_agent_id={chat_agent_id}
|
|
1263
1225
|
{...look}
|
|
1264
1226
|
/>
|
|
1265
1227
|
))}
|
|
1266
1228
|
</View>
|
|
1267
1229
|
) : null}
|
|
1230
|
+
{answered.length > 0 ? (
|
|
1231
|
+
<View style={styles.stageBody}>
|
|
1232
|
+
{answered.map((entry) => (
|
|
1233
|
+
<Text
|
|
1234
|
+
key={`${entry.key}-${entry.since}`}
|
|
1235
|
+
style={styles.muted}
|
|
1236
|
+
>
|
|
1237
|
+
{postedText({ posted: entry })}
|
|
1238
|
+
</Text>
|
|
1239
|
+
))}
|
|
1240
|
+
</View>
|
|
1241
|
+
) : null}
|
|
1268
1242
|
</Section>
|
|
1269
1243
|
|
|
1270
1244
|
{TICKET_STAGES.map((entry) => {
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { PluginTheme } from '@getpaseo/plugin'
|
|
4
|
+
import { usePaseo, useRpc } from '@getpaseo/plugin/client'
|
|
5
|
+
import { copyText, useToast } from '@getpaseo/plugin/client/react-native'
|
|
6
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
|
7
|
+
|
|
8
|
+
import { MONO } from './board-look'
|
|
9
|
+
|
|
10
|
+
import { replyPostRpc } from '../shared/board-rpc'
|
|
11
|
+
import {
|
|
12
|
+
confirmReplyText,
|
|
13
|
+
postedFor,
|
|
14
|
+
postedText,
|
|
15
|
+
replyButtons,
|
|
16
|
+
unstickCommand,
|
|
17
|
+
type PostedReply,
|
|
18
|
+
type ReplyButton,
|
|
19
|
+
} from '../shared/reply-actions'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The actions on stuck work (#503), in the side panel and in the chat's
|
|
23
|
+
* stuck rows: a button per reply the engine takes, which asks to confirm
|
|
24
|
+
* and then posts the reply on the spec issue through `reply.post`, and
|
|
25
|
+
* "Help me", which sends `/luca-unstick` to a chat's agent.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const makeStyles = ({ theme }: { theme: PluginTheme }) => {
|
|
29
|
+
const { colors } = theme
|
|
30
|
+
return StyleSheet.create({
|
|
31
|
+
box: { gap: 6 },
|
|
32
|
+
row: {
|
|
33
|
+
flexDirection: 'row',
|
|
34
|
+
flexWrap: 'wrap',
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
gap: 6,
|
|
37
|
+
},
|
|
38
|
+
label: {
|
|
39
|
+
color: colors.foregroundMuted,
|
|
40
|
+
fontSize: 12,
|
|
41
|
+
fontWeight: '700',
|
|
42
|
+
},
|
|
43
|
+
text: { color: colors.foreground, fontSize: 12 },
|
|
44
|
+
muted: { color: colors.foregroundMuted, fontSize: 12 },
|
|
45
|
+
danger: { color: colors.statusDanger, fontSize: 12 },
|
|
46
|
+
success: { color: colors.statusSuccess, fontSize: 12 },
|
|
47
|
+
button: {
|
|
48
|
+
borderWidth: 1,
|
|
49
|
+
borderColor: colors.border,
|
|
50
|
+
backgroundColor: colors.surface2,
|
|
51
|
+
borderRadius: 6,
|
|
52
|
+
paddingHorizontal: 9,
|
|
53
|
+
paddingVertical: 4,
|
|
54
|
+
},
|
|
55
|
+
buttonText: {
|
|
56
|
+
color: colors.foreground,
|
|
57
|
+
fontSize: 12,
|
|
58
|
+
fontFamily: MONO,
|
|
59
|
+
},
|
|
60
|
+
primary: {
|
|
61
|
+
borderRadius: 6,
|
|
62
|
+
paddingHorizontal: 10,
|
|
63
|
+
paddingVertical: 4,
|
|
64
|
+
backgroundColor: colors.accent,
|
|
65
|
+
},
|
|
66
|
+
primaryText: {
|
|
67
|
+
color: colors.accentForeground,
|
|
68
|
+
fontSize: 12,
|
|
69
|
+
fontWeight: '700',
|
|
70
|
+
},
|
|
71
|
+
posted: { borderColor: colors.statusSuccess },
|
|
72
|
+
postedText: { color: colors.statusSuccess },
|
|
73
|
+
locked: { opacity: 0.45 },
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type Styles = ReturnType<typeof makeStyles>
|
|
78
|
+
|
|
79
|
+
/** What a reply button reads, for its state. */
|
|
80
|
+
const buttonLabel = ({ button }: { button: ReplyButton }): string => {
|
|
81
|
+
switch (button.state) {
|
|
82
|
+
case 'sending':
|
|
83
|
+
return `${button.reply} · posting…`
|
|
84
|
+
case 'posted':
|
|
85
|
+
return `${button.reply} · posted`
|
|
86
|
+
default:
|
|
87
|
+
return button.reply
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ReplyButtonView = ({
|
|
92
|
+
button,
|
|
93
|
+
onPress,
|
|
94
|
+
styles,
|
|
95
|
+
}: {
|
|
96
|
+
button: ReplyButton
|
|
97
|
+
onPress: () => void
|
|
98
|
+
styles: Styles
|
|
99
|
+
}) => (
|
|
100
|
+
<Pressable
|
|
101
|
+
accessibilityRole="button"
|
|
102
|
+
accessibilityLabel={`Post the reply ${button.reply}`}
|
|
103
|
+
accessibilityState={{ disabled: button.state !== 'ready' }}
|
|
104
|
+
disabled={button.state !== 'ready'}
|
|
105
|
+
onPress={onPress}
|
|
106
|
+
style={[
|
|
107
|
+
styles.button,
|
|
108
|
+
button.state === 'posted' ? styles.posted : null,
|
|
109
|
+
button.state === 'locked' ? styles.locked : null,
|
|
110
|
+
]}
|
|
111
|
+
>
|
|
112
|
+
<Text
|
|
113
|
+
style={[
|
|
114
|
+
styles.buttonText,
|
|
115
|
+
button.state === 'posted' ? styles.postedText : null,
|
|
116
|
+
]}
|
|
117
|
+
>
|
|
118
|
+
{buttonLabel({ button })}
|
|
119
|
+
</Text>
|
|
120
|
+
</Pressable>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A stuck item's reply buttons and "Help me". `posted` is what the board
|
|
125
|
+
* says it posted (from `board.read`); a post made here shows at once too.
|
|
126
|
+
* `chat_agent_id` is the chat "Help me" sends `/luca-unstick` to; without
|
|
127
|
+
* one, the command is copied instead.
|
|
128
|
+
*/
|
|
129
|
+
export const StuckActions = ({
|
|
130
|
+
run_id,
|
|
131
|
+
item,
|
|
132
|
+
spec_number,
|
|
133
|
+
posted,
|
|
134
|
+
chat_agent_id,
|
|
135
|
+
theme,
|
|
136
|
+
}: {
|
|
137
|
+
run_id: string
|
|
138
|
+
item: { key: string; ticket: number | null; since: string }
|
|
139
|
+
spec_number: number | null
|
|
140
|
+
posted: PostedReply[]
|
|
141
|
+
chat_agent_id: string | null
|
|
142
|
+
theme: PluginTheme
|
|
143
|
+
}) => {
|
|
144
|
+
const styles = useMemo(() => makeStyles({ theme }), [theme])
|
|
145
|
+
const postReply = useRpc(replyPostRpc)
|
|
146
|
+
const paseo = usePaseo()
|
|
147
|
+
const toast = useToast()
|
|
148
|
+
const [confirming, setConfirming] = useState<string | null>(null)
|
|
149
|
+
const [sending, setSending] = useState<string | null>(null)
|
|
150
|
+
const [mine, setMine] = useState<PostedReply | null>(null)
|
|
151
|
+
const [note, setNote] = useState<{ ok: boolean; text: string } | null>(null)
|
|
152
|
+
|
|
153
|
+
const known = [...posted, ...(mine ? [mine] : [])]
|
|
154
|
+
const buttons = replyButtons({ item, posted: known, sending })
|
|
155
|
+
const done = postedFor({ item, posted: known })
|
|
156
|
+
const command = unstickCommand({ run_id, ticket: item.ticket })
|
|
157
|
+
|
|
158
|
+
const post = async ({ reply }: { reply: string }) => {
|
|
159
|
+
setConfirming(null)
|
|
160
|
+
setSending(reply)
|
|
161
|
+
setNote(null)
|
|
162
|
+
try {
|
|
163
|
+
const output = await postReply({ run_id, key: item.key, reply })
|
|
164
|
+
if (output.posted) setMine(output.posted)
|
|
165
|
+
setNote({ ok: output.ok, text: output.message })
|
|
166
|
+
} catch (error) {
|
|
167
|
+
setNote({
|
|
168
|
+
ok: false,
|
|
169
|
+
text: `Couldn't post \`${reply}\`: ${String(error)}`,
|
|
170
|
+
})
|
|
171
|
+
} finally {
|
|
172
|
+
setSending(null)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const helpMe = async () => {
|
|
177
|
+
if (chat_agent_id !== null) {
|
|
178
|
+
try {
|
|
179
|
+
await paseo.agents.ref(chat_agent_id).send(command)
|
|
180
|
+
toast.show(`Sent ${command} to the run's chat.`, {
|
|
181
|
+
variant: 'success',
|
|
182
|
+
})
|
|
183
|
+
return
|
|
184
|
+
} catch {
|
|
185
|
+
// The chat may be gone: copy it instead.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
await copyText(command)
|
|
190
|
+
toast.show(`Copied ${command}. Paste it in a chat.`, {
|
|
191
|
+
variant: 'success',
|
|
192
|
+
})
|
|
193
|
+
} catch {
|
|
194
|
+
toast.error(`Could not copy. Type ${command} in a chat.`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<View style={styles.box}>
|
|
200
|
+
<Text style={styles.label}>
|
|
201
|
+
Reply on{' '}
|
|
202
|
+
{spec_number === null
|
|
203
|
+
? 'the spec issue'
|
|
204
|
+
: `spec issue #${spec_number}`}
|
|
205
|
+
</Text>
|
|
206
|
+
<View style={styles.row}>
|
|
207
|
+
{buttons.map((button) => (
|
|
208
|
+
<ReplyButtonView
|
|
209
|
+
key={button.reply}
|
|
210
|
+
button={button}
|
|
211
|
+
onPress={() => {
|
|
212
|
+
setNote(null)
|
|
213
|
+
setConfirming(button.reply)
|
|
214
|
+
}}
|
|
215
|
+
styles={styles}
|
|
216
|
+
/>
|
|
217
|
+
))}
|
|
218
|
+
</View>
|
|
219
|
+
{confirming !== null && done === null && sending === null ? (
|
|
220
|
+
<View style={styles.row}>
|
|
221
|
+
<Text style={styles.text}>
|
|
222
|
+
{confirmReplyText({ reply: confirming, spec_number })}
|
|
223
|
+
</Text>
|
|
224
|
+
<Pressable
|
|
225
|
+
accessibilityRole="button"
|
|
226
|
+
accessibilityLabel={`Yes, post ${confirming}`}
|
|
227
|
+
onPress={() => void post({ reply: confirming })}
|
|
228
|
+
style={styles.primary}
|
|
229
|
+
>
|
|
230
|
+
<Text style={styles.primaryText}>Post</Text>
|
|
231
|
+
</Pressable>
|
|
232
|
+
<Pressable
|
|
233
|
+
accessibilityRole="button"
|
|
234
|
+
accessibilityLabel="Cancel"
|
|
235
|
+
onPress={() => setConfirming(null)}
|
|
236
|
+
style={styles.button}
|
|
237
|
+
>
|
|
238
|
+
<Text style={styles.buttonText}>Cancel</Text>
|
|
239
|
+
</Pressable>
|
|
240
|
+
</View>
|
|
241
|
+
) : null}
|
|
242
|
+
{note !== null && !(note.ok && done !== null) ? (
|
|
243
|
+
<Text style={note.ok ? styles.success : styles.danger}>
|
|
244
|
+
{note.text}
|
|
245
|
+
</Text>
|
|
246
|
+
) : null}
|
|
247
|
+
{done !== null ? (
|
|
248
|
+
<Text style={styles.success}>
|
|
249
|
+
{postedText({ posted: done })}
|
|
250
|
+
</Text>
|
|
251
|
+
) : null}
|
|
252
|
+
<View style={styles.row}>
|
|
253
|
+
<Pressable
|
|
254
|
+
accessibilityRole="button"
|
|
255
|
+
accessibilityLabel={`Help me: ${command}`}
|
|
256
|
+
onPress={() => void helpMe()}
|
|
257
|
+
style={styles.button}
|
|
258
|
+
>
|
|
259
|
+
<Text style={styles.buttonText}>Help me</Text>
|
|
260
|
+
</Pressable>
|
|
261
|
+
<Text style={styles.muted}>
|
|
262
|
+
{chat_agent_id === null
|
|
263
|
+
? `copies ${command}`
|
|
264
|
+
: `sends ${command} to the run's chat`}
|
|
265
|
+
</Text>
|
|
266
|
+
</View>
|
|
267
|
+
</View>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
usageColor,
|
|
21
21
|
useNow,
|
|
22
22
|
} from './board-look'
|
|
23
|
+
import { StuckActions } from './stuck-actions'
|
|
23
24
|
|
|
24
25
|
import type { EventRow, LimitRow, RunRow, StuckRow } from '../shared/board-rows'
|
|
25
26
|
import { currentStepText, USAGE_LABEL } from '../shared/board-state'
|
|
@@ -294,9 +295,14 @@ export const EventRowLine = ({
|
|
|
294
295
|
)
|
|
295
296
|
}
|
|
296
297
|
|
|
297
|
-
/**
|
|
298
|
+
/**
|
|
299
|
+
* Stuck work, with a button per reply and "Help me", which sends
|
|
300
|
+
* `/luca-unstick` to this chat (#503); resolved in place. A row added
|
|
301
|
+
* before the buttons shows the replies as text.
|
|
302
|
+
*/
|
|
298
303
|
export const StuckRowCard = ({
|
|
299
304
|
item,
|
|
305
|
+
agentId,
|
|
300
306
|
theme,
|
|
301
307
|
layout,
|
|
302
308
|
}: PluginTimelineItemProps<StuckRow>) => {
|
|
@@ -333,20 +339,41 @@ export const StuckRowCard = ({
|
|
|
333
339
|
· {line}
|
|
334
340
|
</Text>
|
|
335
341
|
))}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
342
|
+
{row.run_id !== null &&
|
|
343
|
+
row.key !== null &&
|
|
344
|
+
row.since !== null ? (
|
|
345
|
+
<StuckActions
|
|
346
|
+
run_id={row.run_id}
|
|
347
|
+
item={{
|
|
348
|
+
key: row.key,
|
|
349
|
+
ticket: row.ticket,
|
|
350
|
+
since: row.since,
|
|
351
|
+
}}
|
|
352
|
+
spec_number={row.spec_number}
|
|
353
|
+
posted={[]}
|
|
354
|
+
chat_agent_id={agentId}
|
|
355
|
+
theme={theme}
|
|
356
|
+
/>
|
|
357
|
+
) : (
|
|
358
|
+
<View style={styles.replyRow}>
|
|
359
|
+
<Text style={styles.muted}>
|
|
360
|
+
Reply with a comment on{' '}
|
|
361
|
+
{row.spec_number === null
|
|
362
|
+
? 'the spec issue'
|
|
363
|
+
: `spec issue #${row.spec_number}`}
|
|
364
|
+
:
|
|
347
365
|
</Text>
|
|
348
|
-
|
|
349
|
-
|
|
366
|
+
{row.replies.map((reply) => (
|
|
367
|
+
<Text
|
|
368
|
+
key={reply}
|
|
369
|
+
style={styles.reply}
|
|
370
|
+
selectable
|
|
371
|
+
>
|
|
372
|
+
{reply}
|
|
373
|
+
</Text>
|
|
374
|
+
))}
|
|
375
|
+
</View>
|
|
376
|
+
)}
|
|
350
377
|
</>
|
|
351
378
|
) : (
|
|
352
379
|
<Text style={styles.muted}>{row.resolution}</Text>
|
package/board/index.server.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
boardReadRpc,
|
|
24
24
|
boardVersionRpc,
|
|
25
25
|
engineEventRpc,
|
|
26
|
+
replyPostRpc,
|
|
26
27
|
runStartRpc,
|
|
27
28
|
} from './shared/board-rpc'
|
|
28
29
|
import { PLUGIN_ID } from './shared/board-state'
|
|
@@ -40,7 +41,8 @@ const log = (message: string) => console.error(`[${PLUGIN_ID}] ${message}`)
|
|
|
40
41
|
* The board plugin's daemon side: the engine settings (their usage lines
|
|
41
42
|
* kept in a file for the engine), `run.start` (launch a
|
|
42
43
|
* run from `/luca-run`), `engine.event` (the engine's journal records in),
|
|
43
|
-
*
|
|
44
|
+
* `board.read` (the side panel's poll), and `reply.post` (a reply button on
|
|
45
|
+
* stuck work posts its word on the spec, #503). On start, and every 15 s after,
|
|
44
46
|
* it checks for runs whose engine is gone and restarts the ones that can go
|
|
45
47
|
* on (#375). The logic lives in `createBoardServer`; this entry only wires it
|
|
46
48
|
* to Paseo.
|
|
@@ -130,6 +132,10 @@ export default function contribute(server: PluginServerContext) {
|
|
|
130
132
|
connect({ context })
|
|
131
133
|
return board.readBoard(input)
|
|
132
134
|
})
|
|
135
|
+
server.handle(replyPostRpc, (input, context) => {
|
|
136
|
+
connect({ context })
|
|
137
|
+
return board.postReply(input)
|
|
138
|
+
})
|
|
133
139
|
// Fixed as loaded: after an upgrade it differs until a reload.
|
|
134
140
|
server.handle(boardVersionRpc, () => ({ version: LUCA_VERSION }))
|
|
135
141
|
|