pi-harness-runtime 1.1.332 → 1.1.334
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/README.md +2 -2
- package/RULES.md +45 -0
- package/harness/skill-sync.ts +11 -2
- package/harness/wiki-integration.ts +6 -6
- package/package.json +3 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/dist/index.js +1 -1
- package/packages/paperclip-integration/dist/index.js.map +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/paperclip-integration/src/index.ts +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/dist/index.js +2 -2
- package/packages/wiki/dist/index.js.map +1 -1
- package/packages/wiki/dist/storage.d.ts +4 -0
- package/packages/wiki/dist/storage.d.ts.map +1 -1
- package/packages/wiki/dist/storage.js +40 -2
- package/packages/wiki/dist/storage.js.map +1 -1
- package/packages/wiki/dist/types.d.ts +1 -0
- package/packages/wiki/dist/types.d.ts.map +1 -1
- package/packages/wiki/dist/types.js.map +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/wiki/src/index.ts +2 -2
- package/packages/wiki/src/storage.ts +41 -2
- package/packages/wiki/src/types.ts +1 -0
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/r2-storage.ts +217 -0
- package/scripts/telegram-mobile-bot.ts +438 -0
- package/scripts/telegram-mobile-integration.ts +480 -0
- package/scripts/test-r2.ts +63 -0
- package/scripts/test-telegram-approval.ts +4 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Mobile Bot - Mobile-First Control Surface
|
|
3
|
+
*
|
|
4
|
+
* Design principles:
|
|
5
|
+
* - ZERO typing - all actions via buttons
|
|
6
|
+
* - One-tap navigation - contextual action buttons
|
|
7
|
+
* - Persistent context - job state always visible
|
|
8
|
+
* - Quick status - current job at a glance
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
const keysDir = path.join(process.env.HOME || "/home/frappe", ".pi-harness-runtime", "keys");
|
|
16
|
+
const dataDir = path.join(process.env.HOME || "/home/frappe", ".pi-harness-runtime", "data");
|
|
17
|
+
|
|
18
|
+
// Load config
|
|
19
|
+
const botToken = fs.readFileSync(path.join(keysDir, "telegram-bot-token.txt"), "utf8").trim();
|
|
20
|
+
const chatId = fs.readFileSync(path.join(keysDir, "telegram-chat-id.txt"), "utf8").trim();
|
|
21
|
+
|
|
22
|
+
// API helpers
|
|
23
|
+
async function sendMessage(text: string, keyboard?: InlineKeyboard): Promise<number | null> {
|
|
24
|
+
const response = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/json" },
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
chat_id: chatId,
|
|
29
|
+
text,
|
|
30
|
+
parse_mode: "HTML",
|
|
31
|
+
reply_markup: keyboard ? { inline_keyboard: keyboard } : undefined,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
const data = await response.json() as { ok: boolean; result?: { message_id: number } };
|
|
35
|
+
return data.ok ? data.result?.message_id ?? null : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function editMessage(messageId: number, text: string, keyboard?: InlineKeyboard): Promise<boolean> {
|
|
39
|
+
const response = await fetch(`https://api.telegram.org/bot${botToken}/editMessageText`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "Content-Type": "application/json" },
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
chat_id: chatId,
|
|
44
|
+
message_id: messageId,
|
|
45
|
+
text,
|
|
46
|
+
parse_mode: "HTML",
|
|
47
|
+
reply_markup: keyboard ? { inline_keyboard: keyboard } : undefined,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
const data = await response.json() as { ok: boolean };
|
|
51
|
+
return data.ok;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function answerCallback(queryId: string, text?: string): Promise<void> {
|
|
55
|
+
await fetch(`https://api.telegram.org/bot${botToken}/answerCallbackQuery`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "Content-Type": "application/json" },
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
callback_query_id: queryId,
|
|
60
|
+
text,
|
|
61
|
+
show_alert: false,
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Types
|
|
67
|
+
type InlineKeyboard = Array<Array<{ text: string; callback_data: string }>>;
|
|
68
|
+
|
|
69
|
+
interface JobStatus {
|
|
70
|
+
id: string;
|
|
71
|
+
title: string;
|
|
72
|
+
status: "pending" | "running" | "done" | "failed";
|
|
73
|
+
progress?: string;
|
|
74
|
+
updatedAt: string;
|
|
75
|
+
messageId?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface JobDetail {
|
|
79
|
+
id: string;
|
|
80
|
+
title: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
status: string;
|
|
83
|
+
logs?: string[];
|
|
84
|
+
diff?: string;
|
|
85
|
+
error?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// State management
|
|
89
|
+
class JobState {
|
|
90
|
+
private stateFile: string;
|
|
91
|
+
private jobs: Map<string, JobStatus> = new Map();
|
|
92
|
+
private currentJobId: string | null = null;
|
|
93
|
+
|
|
94
|
+
constructor() {
|
|
95
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
96
|
+
this.stateFile = path.join(dataDir, "telegram-jobs.json");
|
|
97
|
+
this.load();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private load() {
|
|
101
|
+
try {
|
|
102
|
+
if (existsSync(this.stateFile)) {
|
|
103
|
+
const data = JSON.parse(readFileSync(this.stateFile, "utf8"));
|
|
104
|
+
this.jobs = new Map(data.jobs || []);
|
|
105
|
+
this.currentJobId = data.currentJobId || null;
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error("Failed to load state:", e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private save() {
|
|
113
|
+
writeFileSync(this.stateFile, JSON.stringify({
|
|
114
|
+
jobs: Array.from(this.jobs.entries()),
|
|
115
|
+
currentJobId: this.currentJobId,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setCurrentJob(jobId: string | null) {
|
|
120
|
+
this.currentJobId = jobId;
|
|
121
|
+
this.save();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getCurrentJob(): JobStatus | null {
|
|
125
|
+
return this.currentJobId ? this.jobs.get(this.currentJobId) ?? null : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
updateJob(job: JobStatus) {
|
|
129
|
+
this.jobs.set(job.id, job);
|
|
130
|
+
this.save();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getAllJobs(): JobStatus[] {
|
|
134
|
+
return Array.from(this.jobs.values()).sort(
|
|
135
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
removeJob(jobId: string) {
|
|
140
|
+
this.jobs.delete(jobId);
|
|
141
|
+
if (this.currentJobId === jobId) {
|
|
142
|
+
this.currentJobId = this.jobs.keys().next().value ?? null;
|
|
143
|
+
}
|
|
144
|
+
this.save();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Button generators - NO typing required!
|
|
149
|
+
class ButtonKit {
|
|
150
|
+
static jobCard(job: JobStatus): InlineKeyboard {
|
|
151
|
+
const statusIcon = job.status === "running" ? "🔄" : job.status === "done" ? "✅" : job.status === "failed" ? "❌" : "⏳";
|
|
152
|
+
return [
|
|
153
|
+
[
|
|
154
|
+
{ text: "📋 Details", callback_data: `job:details:${job.id}` },
|
|
155
|
+
{ text: "📜 Logs", callback_data: `job:logs:${job.id}` },
|
|
156
|
+
],
|
|
157
|
+
[
|
|
158
|
+
{ text: "🔄 Retry", callback_data: `job:retry:${job.id}` },
|
|
159
|
+
{ text: "❌ Cancel", callback_data: `job:cancel:${job.id}` },
|
|
160
|
+
],
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static mainMenu(): InlineKeyboard {
|
|
165
|
+
return [
|
|
166
|
+
[
|
|
167
|
+
{ text: "📋 My Jobs", callback_data: "nav:jobs" },
|
|
168
|
+
{ text: "🔍 Search", callback_data: "nav:search" },
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
{ text: "📊 Status", callback_data: "nav:status" },
|
|
172
|
+
{ text: "⚙️ Settings", callback_data: "nav:settings" },
|
|
173
|
+
],
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static jobList(jobs: JobStatus[]): InlineKeyboard {
|
|
178
|
+
const buttons: InlineKeyboard = [];
|
|
179
|
+
for (const job of jobs.slice(0, 5)) {
|
|
180
|
+
const icon = job.status === "running" ? "🔄" : job.status === "done" ? "✅" : job.status === "failed" ? "❌" : "⏳";
|
|
181
|
+
buttons.push([{ text: `${icon} ${job.title.slice(0, 30)}`, callback_data: `job:select:${job.id}` }]);
|
|
182
|
+
}
|
|
183
|
+
buttons.push([{ text: "🔙 Back to Menu", callback_data: "nav:menu" }]);
|
|
184
|
+
return buttons;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static detailsPage(job: JobDetail): InlineKeyboard {
|
|
188
|
+
return [
|
|
189
|
+
[
|
|
190
|
+
{ text: "📜 Full Logs", callback_data: `job:logs:${job.id}` },
|
|
191
|
+
{ text: "📝 View Diff", callback_data: `job:diff:${job.id}` },
|
|
192
|
+
],
|
|
193
|
+
[
|
|
194
|
+
{ text: "🔄 Retry", callback_data: `job:retry:${job.id}` },
|
|
195
|
+
{ text: "🗑️ Dismiss", callback_data: `job:dismiss:${job.id}` },
|
|
196
|
+
],
|
|
197
|
+
[
|
|
198
|
+
{ text: "🔙 Back", callback_data: "nav:jobs" },
|
|
199
|
+
],
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static confirmAction(action: string, targetId: string): InlineKeyboard {
|
|
204
|
+
return [
|
|
205
|
+
[
|
|
206
|
+
{ text: "✅ Confirm", callback_data: `action:confirm:${action}:${targetId}` },
|
|
207
|
+
{ text: "❌ Cancel", callback_data: `action:cancel:${action}:${targetId}` },
|
|
208
|
+
],
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Renderers
|
|
214
|
+
function formatJobCard(job: JobStatus): string {
|
|
215
|
+
const statusIcon = job.status === "running" ? "🔄" : job.status === "done" ? "✅" : job.status === "failed" ? "❌" : "⏳";
|
|
216
|
+
const time = new Date(job.updatedAt).toLocaleTimeString();
|
|
217
|
+
|
|
218
|
+
return `<b>${statusIcon} ${job.title}</b>\n` +
|
|
219
|
+
`Status: ${job.status}${job.progress ? ` (${job.progress})` : ""}\n` +
|
|
220
|
+
`Updated: ${time}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function formatJobDetail(job: JobDetail): string {
|
|
224
|
+
const statusIcon = job.status === "running" ? "🔄" : job.status === "done" ? "✅" : job.status === "failed" ? "❌" : "⏳";
|
|
225
|
+
|
|
226
|
+
let text = `<b>${statusIcon} ${job.title}</b>\n`;
|
|
227
|
+
text += `─────────────────\n`;
|
|
228
|
+
text += `Status: <code>${job.status}</code>\n`;
|
|
229
|
+
|
|
230
|
+
if (job.description) {
|
|
231
|
+
text += `\n${job.description}\n`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (job.error) {
|
|
235
|
+
text += `\n⚠️ <b>Error:</b>\n<code>${job.error.slice(0, 200)}</code>`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (job.logs && job.logs.length > 0) {
|
|
239
|
+
text += `\n\n📜 <b>Recent Logs:</b>`;
|
|
240
|
+
for (const log of job.logs.slice(0, 3)) {
|
|
241
|
+
text += `\n<code>${log.slice(0, 100)}</code>`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return text;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Actions
|
|
249
|
+
async function handleJobDetails(jobId: string): Promise<string> {
|
|
250
|
+
// In real implementation, fetch from job state or API
|
|
251
|
+
return formatJobDetail({
|
|
252
|
+
id: jobId,
|
|
253
|
+
title: "Sample Job",
|
|
254
|
+
status: "running",
|
|
255
|
+
description: "This job is processing...",
|
|
256
|
+
logs: ["Starting process...", "Running tests...", "All tests passed"],
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function handleJobLogs(jobId: string, page = 0): Promise<{ text: string; keyboard: InlineKeyboard }> {
|
|
261
|
+
// In real implementation, fetch from log storage
|
|
262
|
+
const logs = [
|
|
263
|
+
`[10:00] Job started`,
|
|
264
|
+
`[10:01] Running tests...`,
|
|
265
|
+
`[10:02] Test 1: PASS`,
|
|
266
|
+
`[10:02] Test 2: PASS`,
|
|
267
|
+
`[10:03] All 24 tests passed!`,
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
const startIdx = page * 5;
|
|
271
|
+
const pageLogs = logs.slice(startIdx, startIdx + 5);
|
|
272
|
+
|
|
273
|
+
const text = `<b>📜 Job Logs</b>\n<code>${jobId}</code>\n─────────────────\n` +
|
|
274
|
+
pageLogs.map(l => `📍 ${l}`).join("\n") +
|
|
275
|
+
`\n─────────────────\nPage ${page + 1}`;
|
|
276
|
+
|
|
277
|
+
const keyboard: InlineKeyboard = [];
|
|
278
|
+
if (logs.length > startIdx + 5) {
|
|
279
|
+
keyboard.push([{ text: "▶️ Next", callback_data: `job:logs:${jobId}:${page + 1}` }]);
|
|
280
|
+
}
|
|
281
|
+
if (page > 0) {
|
|
282
|
+
keyboard.push([{ text: "◀️ Prev", callback_data: `job:logs:${jobId}:${page - 1}` }]);
|
|
283
|
+
}
|
|
284
|
+
keyboard.push([{ text: "🔙 Back", callback_data: `job:details:${jobId}` }]);
|
|
285
|
+
|
|
286
|
+
return { text, keyboard };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Command handlers - triggered by buttons
|
|
290
|
+
export async function handleCallback(queryId: string, data: string): Promise<void> {
|
|
291
|
+
const parts = data.split(":");
|
|
292
|
+
const action = parts[0];
|
|
293
|
+
const subAction = parts[1];
|
|
294
|
+
const targetId = parts[2];
|
|
295
|
+
|
|
296
|
+
switch (action) {
|
|
297
|
+
case "job": {
|
|
298
|
+
switch (subAction) {
|
|
299
|
+
case "select": {
|
|
300
|
+
// Show job details with action buttons
|
|
301
|
+
const job = new JobState().getAllJobs().find(j => j.id === targetId);
|
|
302
|
+
if (job) {
|
|
303
|
+
const details = await handleJobDetails(targetId);
|
|
304
|
+
await sendMessage(details, ButtonKit.detailsPage({ ...job, description: "" }));
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
case "details": {
|
|
310
|
+
const details = await handleJobDetails(targetId);
|
|
311
|
+
await sendMessage(details, ButtonKit.detailsPage({ id: targetId, title: "", status: "" }));
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
case "logs": {
|
|
316
|
+
const page = parseInt(parts[3] || "0");
|
|
317
|
+
const { text, keyboard } = await handleJobLogs(targetId, page);
|
|
318
|
+
await sendMessage(text, keyboard);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
case "retry": {
|
|
323
|
+
await sendMessage(
|
|
324
|
+
`🔄 Retry <code>${targetId}</code>?`,
|
|
325
|
+
ButtonKit.confirmAction("retry", targetId)
|
|
326
|
+
);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
case "cancel": {
|
|
331
|
+
await sendMessage(
|
|
332
|
+
`❌ Cancel <code>${targetId}</code>?`,
|
|
333
|
+
ButtonKit.confirmAction("cancel", targetId)
|
|
334
|
+
);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
case "dismiss": {
|
|
339
|
+
new JobState().removeJob(targetId);
|
|
340
|
+
await sendMessage("🗑️ Job dismissed", ButtonKit.mainMenu());
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
case "action": {
|
|
348
|
+
if (subAction === "confirm") {
|
|
349
|
+
const actionToConfirm = parts[2];
|
|
350
|
+
const id = parts[3];
|
|
351
|
+
|
|
352
|
+
// Execute the confirmed action
|
|
353
|
+
const jobPipe = path.join(dataDir, "job-commands.jsonl");
|
|
354
|
+
appendFileSync(jobPipe, JSON.stringify({
|
|
355
|
+
command: actionToConfirm,
|
|
356
|
+
jobId: id,
|
|
357
|
+
timestamp: new Date().toISOString(),
|
|
358
|
+
}) + "\n");
|
|
359
|
+
|
|
360
|
+
const messages: Record<string, string> = {
|
|
361
|
+
retry: `🔄 Retrying job <code>${id}</code>...`,
|
|
362
|
+
cancel: `❌ Cancelled job <code>${id}</code>`,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
await sendMessage(messages[actionToConfirm] || "Action completed", ButtonKit.mainMenu());
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
case "nav": {
|
|
371
|
+
switch (subAction) {
|
|
372
|
+
case "menu": {
|
|
373
|
+
await sendMessage(
|
|
374
|
+
"🏠 <b>pi-harness Control Panel</b>\n\nSelect an action:",
|
|
375
|
+
ButtonKit.mainMenu()
|
|
376
|
+
);
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
case "jobs": {
|
|
381
|
+
const jobs = new JobState().getAllJobs();
|
|
382
|
+
if (jobs.length === 0) {
|
|
383
|
+
await sendMessage("📋 <b>No Active Jobs</b>\n\nNo jobs in progress.", ButtonKit.mainMenu());
|
|
384
|
+
} else {
|
|
385
|
+
await sendMessage(
|
|
386
|
+
"📋 <b>Your Jobs</b>\n\nSelect a job:",
|
|
387
|
+
ButtonKit.jobList(jobs)
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
case "status": {
|
|
394
|
+
const current = new JobState().getCurrentJob();
|
|
395
|
+
const text = current
|
|
396
|
+
? formatJobCard(current) + "\n\n<i>Last updated: just now</i>"
|
|
397
|
+
: "✅ <b>All Clear</b>\n\nNo active jobs.";
|
|
398
|
+
await sendMessage(text, ButtonKit.mainMenu());
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
case "search": {
|
|
403
|
+
await sendMessage(
|
|
404
|
+
"🔍 <b>Search</b>\n\nClick a job to search its logs:",
|
|
405
|
+
ButtonKit.jobList(new JobState().getAllJobs())
|
|
406
|
+
);
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case "settings": {
|
|
411
|
+
await sendMessage(
|
|
412
|
+
"⚙️ <b>Settings</b>\n\n" +
|
|
413
|
+
"• Notifications: ON\n" +
|
|
414
|
+
"• Auto-retry: OFF\n" +
|
|
415
|
+
"• Log verbosity: Normal",
|
|
416
|
+
ButtonKit.mainMenu()
|
|
417
|
+
);
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await answerCallback(queryId);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Send initial welcome
|
|
429
|
+
export async function sendWelcome(): Promise<void> {
|
|
430
|
+
await sendMessage(
|
|
431
|
+
"🏠 <b>pi-harness Control Panel</b>\n\n" +
|
|
432
|
+
"Manage your coding tasks from anywhere.\n" +
|
|
433
|
+
"Tap a button below to get started:",
|
|
434
|
+
ButtonKit.mainMenu()
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|