@xl0/pi-lovely-agents 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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/extensions/lovely-agents/agent.ts +1374 -0
- package/extensions/lovely-agents/bash.ts +599 -0
- package/extensions/lovely-agents/child-session.ts +296 -0
- package/extensions/lovely-agents/config.ts +221 -0
- package/extensions/lovely-agents/coordinator.ts +506 -0
- package/extensions/lovely-agents/definitions.ts +380 -0
- package/extensions/lovely-agents/index.ts +400 -0
- package/extensions/lovely-agents/lifecycle.ts +251 -0
- package/extensions/lovely-agents/management.ts +638 -0
- package/extensions/lovely-agents/notifications.ts +220 -0
- package/extensions/lovely-agents/provider-limits.ts +13 -0
- package/extensions/lovely-agents/rendering.ts +90 -0
- package/extensions/lovely-agents/state.ts +1179 -0
- package/extensions/lovely-agents/task-panel.ts +192 -0
- package/extensions/lovely-agents/tools.ts +635 -0
- package/extensions/lovely-agents/updates.ts +45 -0
- package/node_modules/@xl0/pi-lovely-config/CHANGELOG.md +79 -0
- package/node_modules/@xl0/pi-lovely-config/LICENSE +21 -0
- package/node_modules/@xl0/pi-lovely-config/README.md +200 -0
- package/node_modules/@xl0/pi-lovely-config/package.json +59 -0
- package/node_modules/@xl0/pi-lovely-config/src/config.ts +399 -0
- package/node_modules/@xl0/pi-lovely-config/src/index.ts +3 -0
- package/node_modules/@xl0/pi-lovely-config/src/ui.ts +786 -0
- package/package.json +68 -0
- package/skills/agent/SKILL.md +21 -0
- package/skills/agent-creator/SKILL.md +35 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent"
|
|
2
|
+
import { Key, matchesKey, type SelectItem, SelectList, truncateToWidth } from "@earendil-works/pi-tui"
|
|
3
|
+
import type { TaskListResult, TaskListRow } from "./tools.js"
|
|
4
|
+
import { bindTaskUpdateRoute } from "./updates.js"
|
|
5
|
+
|
|
6
|
+
const PANEL_ID = "lovely-agents"
|
|
7
|
+
|
|
8
|
+
/** One below-editor surface: passive active rows, or a focused list of all direct tasks. */
|
|
9
|
+
export function createTaskPanel(
|
|
10
|
+
ctx: ExtensionContext,
|
|
11
|
+
options: { loadTasks: () => Promise<TaskListResult>; openSelection: (value: string) => Promise<void> }
|
|
12
|
+
) {
|
|
13
|
+
let tasks: TaskListResult["tasks"] = []
|
|
14
|
+
let items: SelectItem[] = []
|
|
15
|
+
let capacity = { active: 0, limit: 0 }
|
|
16
|
+
let bashCapacity = { active: 0, limit: 0 }
|
|
17
|
+
let selected: string | undefined
|
|
18
|
+
let focused = false
|
|
19
|
+
let opening = false
|
|
20
|
+
let disposed = false
|
|
21
|
+
let pending: Promise<void> | undefined
|
|
22
|
+
let refreshAgain = false
|
|
23
|
+
|
|
24
|
+
function draw() {
|
|
25
|
+
if (disposed) return
|
|
26
|
+
const active = tasks.filter(task => task.state === "queued" || task.state === "running" || task.state === "suspended")
|
|
27
|
+
const bashCount = active.filter(task => task.kind === "bash").length
|
|
28
|
+
const agentCount = active.length - bashCount
|
|
29
|
+
ctx.ui.setStatus(
|
|
30
|
+
PANEL_ID,
|
|
31
|
+
active.length > 0
|
|
32
|
+
? [
|
|
33
|
+
...(agentCount ? [`agents:${agentCount} slots:${capacity.active}/${capacity.limit}`] : []),
|
|
34
|
+
...(bashCount ? [`bash:${bashCount} slots:${bashCapacity.active}/${bashCapacity.limit}`] : [])
|
|
35
|
+
].join(" ")
|
|
36
|
+
: undefined
|
|
37
|
+
)
|
|
38
|
+
if (opening || (!focused && active.length === 0)) {
|
|
39
|
+
ctx.ui.setWidget(PANEL_ID, undefined)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
ctx.ui.setWidget(
|
|
43
|
+
PANEL_ID,
|
|
44
|
+
(_tui, theme) => ({
|
|
45
|
+
render(width) {
|
|
46
|
+
if (!focused) return renderActiveTaskRows(active).map(line => truncateToWidth(line, width))
|
|
47
|
+
const list = new SelectList(
|
|
48
|
+
items,
|
|
49
|
+
5,
|
|
50
|
+
{
|
|
51
|
+
selectedPrefix: text => theme.fg("accent", text),
|
|
52
|
+
selectedText: text => theme.fg("accent", text),
|
|
53
|
+
description: text => theme.fg("muted", text),
|
|
54
|
+
scrollInfo: text => theme.fg("dim", text),
|
|
55
|
+
noMatch: text => theme.fg("warning", text)
|
|
56
|
+
},
|
|
57
|
+
{ truncatePrimary: ({ text, maxWidth }) => truncateToWidth(text, maxWidth) }
|
|
58
|
+
)
|
|
59
|
+
list.setSelectedIndex(items.findIndex(item => item.value === selected))
|
|
60
|
+
return [
|
|
61
|
+
...(items.length > 0 ? list.render(width) : ["No durable tasks for this session."]),
|
|
62
|
+
theme.fg("dim", "↑↓ navigate · Enter actions · Esc/↑ at top editor · type to edit")
|
|
63
|
+
].map(line => truncateToWidth(line, width))
|
|
64
|
+
},
|
|
65
|
+
invalidate() {}
|
|
66
|
+
}),
|
|
67
|
+
{ placement: "belowEditor" }
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function refresh(): Promise<void> {
|
|
72
|
+
if (disposed) return Promise.resolve()
|
|
73
|
+
refreshAgain = true
|
|
74
|
+
pending ??= (async () => {
|
|
75
|
+
do {
|
|
76
|
+
refreshAgain = false
|
|
77
|
+
const result = await options.loadTasks()
|
|
78
|
+
if (disposed) return
|
|
79
|
+
const oldIndex = items.findIndex(item => item.value === selected)
|
|
80
|
+
tasks = [...result.tasks].sort((left, right) => right.createdAt - left.createdAt || left.id.localeCompare(right.id))
|
|
81
|
+
capacity = result.capacity
|
|
82
|
+
bashCapacity = result.bashCapacity
|
|
83
|
+
items = [
|
|
84
|
+
...tasks.map(task => ({
|
|
85
|
+
value: `task:${task.id}`,
|
|
86
|
+
// One full-width row, not SelectList's fixed-width label/description columns.
|
|
87
|
+
label: (
|
|
88
|
+
`${task.id} ${task.state}${task.latestOutcome ? `/${task.latestOutcome}` : ""} ${task.label} · ` +
|
|
89
|
+
`${task.queueReason ? `waiting: ${task.queueReason} · ` : ""}${task.kind === "bash" ? "bash" : task.model}${task.queuedFollowUps ? ` (+${task.queuedFollowUps})` : ""}` +
|
|
90
|
+
(task.inputPreview ? ` · ${JSON.stringify(task.inputPreview)}` : "")
|
|
91
|
+
).replace(/[\r\n]+/g, " ")
|
|
92
|
+
})),
|
|
93
|
+
...result.diagnostics.map(diagnostic => ({
|
|
94
|
+
value: `diagnostic:${diagnostic.path}`,
|
|
95
|
+
label: `[error] ${diagnostic.id ?? diagnostic.code}`,
|
|
96
|
+
description: diagnostic.message
|
|
97
|
+
}))
|
|
98
|
+
]
|
|
99
|
+
// Keep identity across live reordering; on removal select the nearest remaining row.
|
|
100
|
+
if (!items.some(item => item.value === selected)) {
|
|
101
|
+
selected = items[Math.max(0, Math.min(oldIndex, items.length - 1))]?.value
|
|
102
|
+
}
|
|
103
|
+
draw()
|
|
104
|
+
} while (refreshAgain)
|
|
105
|
+
})().finally(() => {
|
|
106
|
+
pending = undefined
|
|
107
|
+
})
|
|
108
|
+
return pending
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function focus() {
|
|
112
|
+
if (disposed || opening) return
|
|
113
|
+
focused = true
|
|
114
|
+
selected = items[0]?.value
|
|
115
|
+
draw()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function openSelected(value: string) {
|
|
119
|
+
opening = true
|
|
120
|
+
draw()
|
|
121
|
+
try {
|
|
122
|
+
await options.openSelection(value)
|
|
123
|
+
} finally {
|
|
124
|
+
opening = false
|
|
125
|
+
if (!disposed) {
|
|
126
|
+
draw()
|
|
127
|
+
await refresh()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const reportError = (error: unknown) => {
|
|
133
|
+
if (!disposed) ctx.ui.notify(`Lovely Agents task panel: ${error instanceof Error ? error.message : String(error)}`, "error")
|
|
134
|
+
}
|
|
135
|
+
const unbind = bindTaskUpdateRoute(ctx.cwd, ctx.sessionManager.getSessionId(), () => refresh().catch(reportError))
|
|
136
|
+
return {
|
|
137
|
+
refresh,
|
|
138
|
+
focus,
|
|
139
|
+
/** True consumes input; false delegates the original bytes to the existing editor. */
|
|
140
|
+
handleInput(data: string, editorEmpty: boolean): boolean {
|
|
141
|
+
if (disposed) return false
|
|
142
|
+
if (opening) return true
|
|
143
|
+
if (!focused) {
|
|
144
|
+
if (!editorEmpty || !matchesKey(data, Key.down)) return false
|
|
145
|
+
focus()
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
const index = items.findIndex(item => item.value === selected)
|
|
149
|
+
if (matchesKey(data, Key.escape) || (matchesKey(data, Key.up) && index <= 0)) {
|
|
150
|
+
focused = false
|
|
151
|
+
} else if (matchesKey(data, Key.up) || matchesKey(data, Key.down)) {
|
|
152
|
+
const delta = matchesKey(data, Key.up) ? -1 : 1
|
|
153
|
+
selected = items[Math.max(0, Math.min(index + delta, items.length - 1))]?.value
|
|
154
|
+
} else if (matchesKey(data, Key.enter)) {
|
|
155
|
+
if (selected) void openSelected(selected).catch(reportError)
|
|
156
|
+
} else {
|
|
157
|
+
focused = false
|
|
158
|
+
draw()
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
draw()
|
|
162
|
+
return true
|
|
163
|
+
},
|
|
164
|
+
dispose() {
|
|
165
|
+
if (disposed) return
|
|
166
|
+
disposed = true
|
|
167
|
+
unbind()
|
|
168
|
+
ctx.ui.setStatus(PANEL_ID, undefined)
|
|
169
|
+
ctx.ui.setWidget(PANEL_ID, undefined)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function renderActiveTaskRows(
|
|
175
|
+
tasks: readonly ({
|
|
176
|
+
id: string
|
|
177
|
+
label: string
|
|
178
|
+
state: string
|
|
179
|
+
queuedFollowUps: number
|
|
180
|
+
} & Partial<Pick<TaskListRow, "queueReason" | "inputPreview">>)[]
|
|
181
|
+
): string[] {
|
|
182
|
+
const rows = tasks
|
|
183
|
+
.slice(0, 5)
|
|
184
|
+
.map(
|
|
185
|
+
task =>
|
|
186
|
+
`↳ ${task.id} ${task.state} ${task.label.replace(/[\r\n]+/g, " ")}${task.queuedFollowUps ? ` (+${task.queuedFollowUps})` : ""}` +
|
|
187
|
+
(task.queueReason ? ` · waiting: ${task.queueReason}` : "") +
|
|
188
|
+
(task.inputPreview ? ` · ${JSON.stringify(task.inputPreview)}` : "")
|
|
189
|
+
)
|
|
190
|
+
if (tasks.length > rows.length) rows.push(` … ${tasks.length - rows.length} more active`)
|
|
191
|
+
return rows
|
|
192
|
+
}
|