@clubnet/seedclub 0.2.21 → 0.2.23
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/assets/extensions/seedclub/branding.ts +16 -16
- package/assets/extensions/seedclub/commands/transcript-intent.ts +338 -129
- package/assets/extensions/seedclub/commands/transcripts.ts +1 -3
- package/assets/extensions/seedclub/tool-utils.ts +74 -0
- package/assets/extensions/seedclub/tools/crm.ts +35 -1
- package/assets/extensions/seedclub/tools/media.ts +121 -6
- package/assets/extensions/seedclub/tools/meetings.ts +80 -6
- package/assets/extensions/seedclub/tools/utility.ts +12 -1
- package/assets/extensions/seedclub-ui/editor.ts +6 -1
- package/assets/extensions/seedclub-ui/index.ts +2 -0
- package/assets/extensions/seedclub-ui/tool-progress.ts +39 -0
- package/assets/extensions/seedclub-ui/welcome.ts +23 -2
- package/assets/theme/dark.json +1 -1
- package/assets/theme/light.json +1 -1
- package/bin/cli.js +2 -2
- package/bin/pi-main-launcher.js +20 -6
- package/package.json +1 -1
|
@@ -168,7 +168,7 @@ function resolveOutputDir(programSlug: string, requested?: string): string {
|
|
|
168
168
|
const raw = requested.replace(/^~(?=$|\/)/, homedir());
|
|
169
169
|
return resolve(raw);
|
|
170
170
|
}
|
|
171
|
-
return join(homedir(), "Downloads", "
|
|
171
|
+
return join(homedir(), "Downloads", sanitizeFilePart(programSlug || "transcripts", "transcripts"));
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
function parseTimeFilter(input?: string): { hour: number; minute?: number } | null {
|
|
@@ -363,8 +363,6 @@ export function registerTranscriptsCommand(pi: ExtensionAPI) {
|
|
|
363
363
|
return;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
await writeFile(join(outDir, "index.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
367
|
-
|
|
368
366
|
const preview = manifest
|
|
369
367
|
.slice(0, 8)
|
|
370
368
|
.map((entry) => ` ${entry.file}`)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Returns JSON results on success, clear error messages on failure.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
6
7
|
import { NotConnectedError } from "./api-client.js";
|
|
7
8
|
|
|
8
9
|
const MAX_TOOL_RESULT_DEPTH = 6;
|
|
@@ -46,6 +47,79 @@ function compactToolResult(value: any, depth = 0): any {
|
|
|
46
47
|
return Object.fromEntries(entries.map(([key, item]) => [key, compactToolResult(item, depth + 1)]));
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
51
|
+
|
|
52
|
+
export function makeProgressCallRenderer(
|
|
53
|
+
label: string,
|
|
54
|
+
detail?: (args: any) => string | undefined,
|
|
55
|
+
) {
|
|
56
|
+
return (args: any, theme: any, context: any) => {
|
|
57
|
+
const state = (context.state ??= {});
|
|
58
|
+
if (context.isPartial) {
|
|
59
|
+
if (!state.spinnerTimer) {
|
|
60
|
+
state.spinnerFrame = 0;
|
|
61
|
+
state.spinnerTimer = setInterval(() => {
|
|
62
|
+
state.spinnerFrame = ((state.spinnerFrame ?? 0) + 1) % SPINNER_FRAMES.length;
|
|
63
|
+
context.invalidate();
|
|
64
|
+
}, 90);
|
|
65
|
+
state.spinnerTimer.unref?.();
|
|
66
|
+
}
|
|
67
|
+
} else if (state.spinnerTimer) {
|
|
68
|
+
clearInterval(state.spinnerTimer);
|
|
69
|
+
state.spinnerTimer = undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const frame = context.isPartial ? SPINNER_FRAMES[state.spinnerFrame ?? 0] : "✓";
|
|
73
|
+
const tone = context.isPartial ? "accent" : context.isError ? "error" : "success";
|
|
74
|
+
let text = `${theme.fg(tone, frame)} ${theme.fg("toolTitle", label)}`;
|
|
75
|
+
const detailText = detail?.(args);
|
|
76
|
+
if (detailText) text += theme.fg("dim", ` · ${detailText}`);
|
|
77
|
+
return new Text(text, 0, 0);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function summarizeDetails(details: any): string | undefined {
|
|
82
|
+
if (!details || typeof details !== "object") return undefined;
|
|
83
|
+
if (typeof details.error === "string" && details.error.trim()) return details.error.trim();
|
|
84
|
+
if (Array.isArray(details.data)) return `${details.data.length} row${details.data.length === 1 ? "" : "s"}`;
|
|
85
|
+
if (Array.isArray(details.records)) return `${details.records.length} record${details.records.length === 1 ? "" : "s"}`;
|
|
86
|
+
if (typeof details.count === "number") return `${details.count} item${details.count === 1 ? "" : "s"}`;
|
|
87
|
+
if (typeof details.total === "number") return `${details.total} total`;
|
|
88
|
+
if (details.program?.slug) return String(details.program.slug);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function firstTextLine(content: Array<{ type: string; text?: string }> | undefined): string | null {
|
|
93
|
+
if (!Array.isArray(content)) return null;
|
|
94
|
+
const text = content.find((item) => item?.type === "text" && typeof item.text === "string")?.text?.trim();
|
|
95
|
+
if (!text) return null;
|
|
96
|
+
return text.split("\n")[0] ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function makeProgressResultRenderer(
|
|
100
|
+
successLabel = "Completed",
|
|
101
|
+
summary?: (details: any, args: any) => string | undefined,
|
|
102
|
+
) {
|
|
103
|
+
return (result: any, options: any, theme: any, context: any) => {
|
|
104
|
+
const state = context.state ?? {};
|
|
105
|
+
if (state.spinnerTimer) {
|
|
106
|
+
clearInterval(state.spinnerTimer);
|
|
107
|
+
state.spinnerTimer = undefined;
|
|
108
|
+
}
|
|
109
|
+
if (options?.isPartial) {
|
|
110
|
+
return new Text(theme.fg("dim", "Working..."), 0, 0);
|
|
111
|
+
}
|
|
112
|
+
if (result?.isError) {
|
|
113
|
+
const line = firstTextLine(result?.content) ?? "Request failed";
|
|
114
|
+
return new Text(`${theme.fg("error", "✕")} ${theme.fg("error", line)}`, 0, 0);
|
|
115
|
+
}
|
|
116
|
+
const detailText = summary?.(result?.details, context?.args) ?? summarizeDetails(result?.details);
|
|
117
|
+
let text = `${theme.fg("success", "✓")} ${theme.fg("muted", successLabel)}`;
|
|
118
|
+
if (detailText) text += theme.fg("dim", ` · ${detailText}`);
|
|
119
|
+
return new Text(text, 0, 0);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
49
123
|
export function wrapExecute(fn: (params: any) => Promise<any>) {
|
|
50
124
|
return async (_toolCallId: string, params: any, _signal: any, _onUpdate: any, _ctx: any) => {
|
|
51
125
|
try {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
2
3
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
4
|
import { ApiError, api } from "../api-client.js";
|
|
4
|
-
import { wrapExecute } from "../tool-utils.js";
|
|
5
|
+
import { makeProgressCallRenderer, makeProgressResultRenderer, wrapExecute } from "../tool-utils.js";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_RECORD_LIMIT = 5;
|
|
7
8
|
const MAX_RECORD_LIMIT = 50;
|
|
@@ -132,6 +133,11 @@ export function registerCrmTools(pi: ExtensionAPI) {
|
|
|
132
133
|
),
|
|
133
134
|
}),
|
|
134
135
|
execute: wrapExecute(listCrmRecords),
|
|
136
|
+
renderCall: makeProgressCallRenderer("Loading CRM records"),
|
|
137
|
+
renderResult: makeProgressResultRenderer("CRM records loaded", (details) => {
|
|
138
|
+
const count = Array.isArray(details?.records) ? details.records.length : details?.total;
|
|
139
|
+
return typeof count === "number" ? `${count} record${count === 1 ? "" : "s"}` : undefined;
|
|
140
|
+
}),
|
|
135
141
|
});
|
|
136
142
|
|
|
137
143
|
pi.registerTool({
|
|
@@ -142,6 +148,8 @@ export function registerCrmTools(pi: ExtensionAPI) {
|
|
|
142
148
|
recordId: Type.String({ description: "CRM record id" }),
|
|
143
149
|
}),
|
|
144
150
|
execute: wrapExecute(getCrmRecord),
|
|
151
|
+
renderCall: makeProgressCallRenderer("Loading CRM record", (args) => args?.recordId || undefined),
|
|
152
|
+
renderResult: makeProgressResultRenderer("CRM record loaded", (details) => details?.record?.id || undefined),
|
|
145
153
|
});
|
|
146
154
|
|
|
147
155
|
pi.registerTool({
|
|
@@ -154,6 +162,8 @@ export function registerCrmTools(pi: ExtensionAPI) {
|
|
|
154
162
|
noteType: Type.Optional(Type.String({ description: "Optional note type" })),
|
|
155
163
|
}),
|
|
156
164
|
execute: wrapExecute(createCrmNote),
|
|
165
|
+
renderCall: makeProgressCallRenderer("Saving CRM note", (args) => args?.recordId || undefined),
|
|
166
|
+
renderResult: makeProgressResultRenderer("CRM note saved"),
|
|
157
167
|
});
|
|
158
168
|
|
|
159
169
|
pi.registerTool({
|
|
@@ -168,6 +178,8 @@ export function registerCrmTools(pi: ExtensionAPI) {
|
|
|
168
178
|
assignedUserId: Type.Optional(Type.String({ description: "Optional assigned user id" })),
|
|
169
179
|
}),
|
|
170
180
|
execute: wrapExecute(createCrmTask),
|
|
181
|
+
renderCall: makeProgressCallRenderer("Creating CRM task", (args) => args?.recordId || undefined),
|
|
182
|
+
renderResult: makeProgressResultRenderer("CRM task created"),
|
|
171
183
|
});
|
|
172
184
|
|
|
173
185
|
pi.registerTool({
|
|
@@ -179,5 +191,27 @@ export function registerCrmTools(pi: ExtensionAPI) {
|
|
|
179
191
|
search: Type.Optional(Type.String({ description: "Optional contact search text" })),
|
|
180
192
|
}),
|
|
181
193
|
execute: wrapExecute(listProgramContacts),
|
|
194
|
+
renderCall: makeProgressCallRenderer("Loading program contacts", (args) => args?.programSlug || undefined),
|
|
195
|
+
renderResult(result: any, _args: any, theme: any) {
|
|
196
|
+
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
197
|
+
const details = result.details ?? {};
|
|
198
|
+
const rows = Array.isArray(details?.data) ? details.data : [];
|
|
199
|
+
const program = details?.program?.slug ?? details?.program?.name ?? "program";
|
|
200
|
+
if (!rows.length) return new Text(theme.fg("dim", `No contacts found for ${program}`), 0, 0);
|
|
201
|
+
|
|
202
|
+
const shown = rows.slice(0, 8);
|
|
203
|
+
let text = theme.fg("muted", `${rows.length} contact${rows.length === 1 ? "" : "s"} loaded for ${program}`);
|
|
204
|
+
for (const row of shown) {
|
|
205
|
+
const name = row?.party?.display_name ?? row?.person?.full_name ?? "Unknown";
|
|
206
|
+
const org = row?.roles?.[0]?.organization_name ?? row?.organization?.name ?? null;
|
|
207
|
+
const role = row?.roles?.[0]?.role ?? row?.organization_role ?? null;
|
|
208
|
+
const detail = [org, role].filter(Boolean).join(" · ");
|
|
209
|
+
text += `\n ${name}${detail ? ` — ${detail}` : ""}`;
|
|
210
|
+
}
|
|
211
|
+
if (rows.length > shown.length) {
|
|
212
|
+
text += theme.fg("dim", `\n ...and ${rows.length - shown.length} more in this response`);
|
|
213
|
+
}
|
|
214
|
+
return new Text(text, 0, 0);
|
|
215
|
+
},
|
|
182
216
|
});
|
|
183
217
|
}
|
|
@@ -1,7 +1,60 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
3
|
import { ApiError, api } from "../api-client.js";
|
|
4
|
-
import { wrapExecute } from "../tool-utils.js";
|
|
4
|
+
import { makeProgressCallRenderer, makeProgressResultRenderer, wrapExecute } from "../tool-utils.js";
|
|
5
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
6
|
+
|
|
7
|
+
const TRANSCRIPT_TEXT_PREVIEW_CHARS = 1200;
|
|
8
|
+
const TRANSCRIPT_VTT_PREVIEW_CHARS = 800;
|
|
9
|
+
|
|
10
|
+
function truncateText(value: string | null | undefined, maxChars: number) {
|
|
11
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
12
|
+
if (value.length <= maxChars) return value;
|
|
13
|
+
return `${value.slice(0, maxChars)}\n...[truncated ${value.length - maxChars} chars]`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function shapeAssetTranscriptFields(asset: any, includeFullText: boolean) {
|
|
17
|
+
const transcriptRaw = typeof asset?.transcript_raw === "string" ? asset.transcript_raw : null;
|
|
18
|
+
const transcriptEdited = typeof asset?.transcript_edited === "string" ? asset.transcript_edited : null;
|
|
19
|
+
const transcriptVtt = typeof asset?.transcript_vtt === "string" ? asset.transcript_vtt : null;
|
|
20
|
+
const effectiveText = transcriptEdited ?? transcriptRaw;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
has_transcript_text: !!effectiveText?.trim(),
|
|
24
|
+
has_transcript_vtt: !!transcriptVtt?.trim(),
|
|
25
|
+
transcript_text_length: effectiveText?.length ?? 0,
|
|
26
|
+
transcript_vtt_length: transcriptVtt?.length ?? 0,
|
|
27
|
+
...(includeFullText
|
|
28
|
+
? {
|
|
29
|
+
transcript_edited: transcriptEdited,
|
|
30
|
+
transcript_raw: transcriptRaw,
|
|
31
|
+
transcript_vtt: transcriptVtt,
|
|
32
|
+
}
|
|
33
|
+
: {
|
|
34
|
+
transcript_text_preview: truncateText(effectiveText, TRANSCRIPT_TEXT_PREVIEW_CHARS),
|
|
35
|
+
transcript_vtt_preview: truncateText(transcriptVtt, TRANSCRIPT_VTT_PREVIEW_CHARS),
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function shapeMediaAssetRow(row: any, includeFullText: boolean) {
|
|
41
|
+
if (!row || typeof row !== "object") return row;
|
|
42
|
+
const asset = row.asset ?? row;
|
|
43
|
+
const shapedAsset = {
|
|
44
|
+
...asset,
|
|
45
|
+
...shapeAssetTranscriptFields(asset, includeFullText),
|
|
46
|
+
};
|
|
47
|
+
if (!includeFullText) {
|
|
48
|
+
delete shapedAsset.transcript_edited;
|
|
49
|
+
delete shapedAsset.transcript_raw;
|
|
50
|
+
delete shapedAsset.transcript_vtt;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (row.asset) {
|
|
54
|
+
return { ...row, asset: shapedAsset };
|
|
55
|
+
}
|
|
56
|
+
return shapedAsset;
|
|
57
|
+
}
|
|
5
58
|
|
|
6
59
|
async function listProgramMediaAssets(args: {
|
|
7
60
|
programSlug: string;
|
|
@@ -13,9 +66,11 @@ async function listProgramMediaAssets(args: {
|
|
|
13
66
|
partyId?: string;
|
|
14
67
|
visibility?: string;
|
|
15
68
|
workflowStatus?: string;
|
|
69
|
+
includeFullText?: boolean;
|
|
16
70
|
}) {
|
|
17
71
|
try {
|
|
18
|
-
|
|
72
|
+
const includeFullText = args.includeFullText === true;
|
|
73
|
+
const response = await api.get<any>(`/programs/${args.programSlug}/media/assets`, {
|
|
19
74
|
asset_kind: args.assetKind,
|
|
20
75
|
event_date: args.eventDate,
|
|
21
76
|
limit: args.limit,
|
|
@@ -25,15 +80,24 @@ async function listProgramMediaAssets(args: {
|
|
|
25
80
|
visibility: args.visibility,
|
|
26
81
|
workflow_status: args.workflowStatus,
|
|
27
82
|
});
|
|
83
|
+
return {
|
|
84
|
+
...response,
|
|
85
|
+
data: Array.isArray(response?.data) ? response.data.map((row: any) => shapeMediaAssetRow(row, includeFullText)) : [],
|
|
86
|
+
};
|
|
28
87
|
} catch (error) {
|
|
29
88
|
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
30
89
|
throw error;
|
|
31
90
|
}
|
|
32
91
|
}
|
|
33
92
|
|
|
34
|
-
async function getProgramMediaAsset(args: { programSlug: string; assetId: string }) {
|
|
93
|
+
async function getProgramMediaAsset(args: { programSlug: string; assetId: string; includeFullText?: boolean }) {
|
|
35
94
|
try {
|
|
36
|
-
|
|
95
|
+
const includeFullText = args.includeFullText === true;
|
|
96
|
+
const response = await api.get<any>(`/programs/${args.programSlug}/media/assets/${args.assetId}`);
|
|
97
|
+
return {
|
|
98
|
+
...response,
|
|
99
|
+
asset: shapeMediaAssetRow(response?.asset, includeFullText),
|
|
100
|
+
};
|
|
37
101
|
} catch (error) {
|
|
38
102
|
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
39
103
|
throw error;
|
|
@@ -124,7 +188,7 @@ export function registerMediaTools(pi: ExtensionAPI) {
|
|
|
124
188
|
name: "seedclub_list_program_media_assets",
|
|
125
189
|
label: "List Program Media Assets",
|
|
126
190
|
description:
|
|
127
|
-
"List media assets for a specific Seed Club program. Use assetKind=full_conversation when the user asks for full conversations, because those asset rows can include
|
|
191
|
+
"List media assets for a specific Seed Club program. Use assetKind=full_conversation when the user asks for full conversations, because those asset rows can include transcript fields and media URLs. By default this tool returns compact transcript metadata/previews (not full transcript text) to keep payloads small; set includeFullText=true only when full transcript fields are explicitly needed. Do not use this as the default source for broad transcript inventory requests; use seedclub_list_meeting_transcripts first for requests like 'all transcripts on 11am'. Use assetKind=clip when the user asks for published clips. Read the response total field for count questions, and use limit plus offset when the user wants paging or more results.",
|
|
128
192
|
parameters: Type.Object({
|
|
129
193
|
programSlug: Type.String({ description: "Program slug" }),
|
|
130
194
|
assetKind: Type.Optional(
|
|
@@ -147,20 +211,62 @@ export function registerMediaTools(pi: ExtensionAPI) {
|
|
|
147
211
|
partyId: Type.Optional(Type.String({ description: "Optional party id filter" })),
|
|
148
212
|
visibility: Type.Optional(Type.String({ description: "Optional visibility filter" })),
|
|
149
213
|
workflowStatus: Type.Optional(Type.String({ description: "Optional workflow status filter" })),
|
|
214
|
+
includeFullText: Type.Optional(
|
|
215
|
+
Type.Boolean({
|
|
216
|
+
description: "Set true only when full transcript text/VTT fields are required. Defaults to compact previews.",
|
|
217
|
+
}),
|
|
218
|
+
),
|
|
150
219
|
}),
|
|
151
220
|
execute: wrapExecute(listProgramMediaAssets),
|
|
221
|
+
renderCall: makeProgressCallRenderer("Checking media assets", (args) => args?.programSlug || undefined),
|
|
222
|
+
renderResult(result: any, { expanded }: any, theme: any) {
|
|
223
|
+
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
224
|
+
const rows = Array.isArray(result.details?.data) ? result.details.data : [];
|
|
225
|
+
const program = result.details?.program?.slug ?? result.details?.program?.name ?? "program";
|
|
226
|
+
if (!rows.length) return new Text(theme.fg("dim", `No media assets for ${program}`), 0, 0);
|
|
227
|
+
|
|
228
|
+
let text = theme.fg("muted", `${rows.length} media asset${rows.length === 1 ? "" : "s"} for ${program}`);
|
|
229
|
+
for (const row of rows.slice(0, expanded ? 12 : 6)) {
|
|
230
|
+
const asset = row?.asset ?? row;
|
|
231
|
+
const date = asset?.event_date ?? "unknown date";
|
|
232
|
+
const title = asset?.title ?? asset?.file_name ?? "untitled";
|
|
233
|
+
const kind = asset?.asset_kind ?? "asset";
|
|
234
|
+
const hasText = asset?.has_transcript_text ? "text" : "no-text";
|
|
235
|
+
const hasVtt = asset?.has_transcript_vtt ? "vtt" : "no-vtt";
|
|
236
|
+
text += `\n ${date} - ${title} [${kind}; ${hasText}; ${hasVtt}]`;
|
|
237
|
+
}
|
|
238
|
+
if (!expanded && rows.length > 6) text += theme.fg("dim", `\n ...and ${rows.length - 6} more`);
|
|
239
|
+
return new Text(text, 0, 0);
|
|
240
|
+
},
|
|
152
241
|
});
|
|
153
242
|
|
|
154
243
|
pi.registerTool({
|
|
155
244
|
name: "seedclub_get_program_media_asset",
|
|
156
245
|
label: "Get Program Media Asset",
|
|
157
246
|
description:
|
|
158
|
-
"Load one program-scoped media asset detail by asset id.
|
|
247
|
+
"Load one program-scoped media asset detail by asset id. Returns compact transcript metadata/previews by default; set includeFullText=true when full transcript text or VTT is explicitly needed.",
|
|
159
248
|
parameters: Type.Object({
|
|
160
249
|
programSlug: Type.String({ description: "Program slug" }),
|
|
161
250
|
assetId: Type.String({ description: "Asset id" }),
|
|
251
|
+
includeFullText: Type.Optional(
|
|
252
|
+
Type.Boolean({
|
|
253
|
+
description: "Set true only when full transcript text/VTT fields are required. Defaults to compact previews.",
|
|
254
|
+
}),
|
|
255
|
+
),
|
|
162
256
|
}),
|
|
163
257
|
execute: wrapExecute(getProgramMediaAsset),
|
|
258
|
+
renderCall: makeProgressCallRenderer("Loading media asset details", (args) => args?.assetId || undefined),
|
|
259
|
+
renderResult(result: any, _args: any, theme: any) {
|
|
260
|
+
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
261
|
+
const asset = result.details?.asset ?? null;
|
|
262
|
+
if (!asset) return new Text(theme.fg("dim", "No media asset found"), 0, 0);
|
|
263
|
+
const date = asset?.event_date ?? "unknown date";
|
|
264
|
+
const title = asset?.title ?? asset?.file_name ?? "untitled";
|
|
265
|
+
const kind = asset?.asset_kind ?? "asset";
|
|
266
|
+
const hasText = asset?.has_transcript_text ? "text" : "no-text";
|
|
267
|
+
const hasVtt = asset?.has_transcript_vtt ? "vtt" : "no-vtt";
|
|
268
|
+
return new Text(theme.fg("muted", `${date} - ${title} [${kind}; ${hasText}; ${hasVtt}]`), 0, 0);
|
|
269
|
+
},
|
|
164
270
|
});
|
|
165
271
|
|
|
166
272
|
pi.registerTool({
|
|
@@ -173,6 +279,11 @@ export function registerMediaTools(pi: ExtensionAPI) {
|
|
|
173
279
|
slot: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
174
280
|
}),
|
|
175
281
|
execute: wrapExecute(listProgramHeadlines),
|
|
282
|
+
renderCall: makeProgressCallRenderer("Loading program headlines", (args) => args?.date || undefined),
|
|
283
|
+
renderResult: makeProgressResultRenderer("Program headlines loaded", (details) => {
|
|
284
|
+
const count = Array.isArray(details?.data) ? details.data.length : undefined;
|
|
285
|
+
return typeof count === "number" ? `${count} headline${count === 1 ? "" : "s"}` : undefined;
|
|
286
|
+
}),
|
|
176
287
|
});
|
|
177
288
|
|
|
178
289
|
pi.registerTool({
|
|
@@ -192,6 +303,8 @@ export function registerMediaTools(pi: ExtensionAPI) {
|
|
|
192
303
|
isLive: Type.Optional(Type.Boolean({ description: "Optional live flag" })),
|
|
193
304
|
}),
|
|
194
305
|
execute: wrapExecute(createProgramHeadline),
|
|
306
|
+
renderCall: makeProgressCallRenderer("Saving headline", (args) => args?.date || undefined),
|
|
307
|
+
renderResult: makeProgressResultRenderer("Headline saved"),
|
|
195
308
|
});
|
|
196
309
|
|
|
197
310
|
pi.registerTool({
|
|
@@ -213,5 +326,7 @@ export function registerMediaTools(pi: ExtensionAPI) {
|
|
|
213
326
|
isLive: Type.Optional(Type.Boolean({ description: "Optional live flag" })),
|
|
214
327
|
}),
|
|
215
328
|
execute: wrapExecute(updateProgramHeadline),
|
|
329
|
+
renderCall: makeProgressCallRenderer("Updating headline", (args) => args?.headlineId || undefined),
|
|
330
|
+
renderResult: makeProgressResultRenderer("Headline updated"),
|
|
216
331
|
});
|
|
217
332
|
}
|
|
@@ -2,7 +2,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
2
2
|
import { Text } from "@mariozechner/pi-tui";
|
|
3
3
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { ApiError, api } from "../api-client.js";
|
|
5
|
-
import { wrapExecute } from "../tool-utils.js";
|
|
5
|
+
import { makeProgressCallRenderer, makeProgressResultRenderer, wrapExecute } from "../tool-utils.js";
|
|
6
6
|
|
|
7
7
|
const DEFAULT_MEETINGS_LIMIT = 10;
|
|
8
8
|
const MAX_MEETINGS_LIMIT = 25;
|
|
@@ -1072,6 +1072,11 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1072
1072
|
limit: Type.Optional(Type.Number({ description: "Optional max meetings to return. Defaults to 10, max 25." })),
|
|
1073
1073
|
}),
|
|
1074
1074
|
execute: wrapExecute(listMeetings),
|
|
1075
|
+
renderCall: makeProgressCallRenderer("Checking meeting schedule", (args) => args?.programSlug || undefined),
|
|
1076
|
+
renderResult: makeProgressResultRenderer("Meeting schedule loaded", (details) => {
|
|
1077
|
+
const count = Array.isArray(details?.data) ? details.data.length : undefined;
|
|
1078
|
+
return typeof count === "number" ? `${count} meeting${count === 1 ? "" : "s"}` : undefined;
|
|
1079
|
+
}),
|
|
1075
1080
|
});
|
|
1076
1081
|
|
|
1077
1082
|
pi.registerTool({
|
|
@@ -1092,6 +1097,11 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1092
1097
|
),
|
|
1093
1098
|
}),
|
|
1094
1099
|
execute: wrapExecute(listShowGuests),
|
|
1100
|
+
renderCall: makeProgressCallRenderer("Looking up show guests", (args) => args?.programSlug || undefined),
|
|
1101
|
+
renderResult: makeProgressResultRenderer("Show guests loaded", (details) => {
|
|
1102
|
+
const count = Array.isArray(details?.data) ? details.data.length : undefined;
|
|
1103
|
+
return typeof count === "number" ? `${count} guest row${count === 1 ? "" : "s"}` : undefined;
|
|
1104
|
+
}),
|
|
1095
1105
|
});
|
|
1096
1106
|
|
|
1097
1107
|
pi.registerTool({
|
|
@@ -1118,6 +1128,11 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1118
1128
|
),
|
|
1119
1129
|
}),
|
|
1120
1130
|
execute: wrapExecute(listGuestRoster),
|
|
1131
|
+
renderCall: makeProgressCallRenderer("Building guest roster", (args) => args?.programSlug || undefined),
|
|
1132
|
+
renderResult: makeProgressResultRenderer("Guest roster ready", (details) => {
|
|
1133
|
+
const count = Array.isArray(details?.rows) ? details.rows.length : undefined;
|
|
1134
|
+
return typeof count === "number" ? `${count} guest${count === 1 ? "" : "s"}` : undefined;
|
|
1135
|
+
}),
|
|
1121
1136
|
});
|
|
1122
1137
|
|
|
1123
1138
|
pi.registerTool({
|
|
@@ -1138,6 +1153,8 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1138
1153
|
),
|
|
1139
1154
|
}),
|
|
1140
1155
|
execute: wrapExecute(getGuestProfile),
|
|
1156
|
+
renderCall: makeProgressCallRenderer("Resolving guest profile", (args) => args?.search || args?.email || args?.partyId || undefined),
|
|
1157
|
+
renderResult: makeProgressResultRenderer("Guest profile loaded", (details) => details?.contact?.displayName || undefined),
|
|
1141
1158
|
});
|
|
1142
1159
|
|
|
1143
1160
|
pi.registerTool({
|
|
@@ -1165,6 +1182,7 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1165
1182
|
),
|
|
1166
1183
|
}),
|
|
1167
1184
|
execute: wrapExecute(findLatestGuestTranscript),
|
|
1185
|
+
renderCall: makeProgressCallRenderer("Finding latest guest transcript", (args) => args?.guest || undefined),
|
|
1168
1186
|
renderResult(result: any, _args: any, theme: any) {
|
|
1169
1187
|
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
1170
1188
|
const found = result.details?.found === true;
|
|
@@ -1192,6 +1210,34 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1192
1210
|
),
|
|
1193
1211
|
}),
|
|
1194
1212
|
execute: wrapExecute(prepareClipPacket),
|
|
1213
|
+
renderCall: makeProgressCallRenderer("Checking clip readiness", (args) => args?.meetingId || undefined),
|
|
1214
|
+
renderResult(result: any, _args: any, theme: any) {
|
|
1215
|
+
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
1216
|
+
|
|
1217
|
+
const details = result.details ?? {};
|
|
1218
|
+
const meeting = details.meeting ?? {};
|
|
1219
|
+
const guestName =
|
|
1220
|
+
details?.guestProfile?.contact?.displayName ??
|
|
1221
|
+
details?.guestProfile?.query?.search ??
|
|
1222
|
+
details?.guestProfile?.query?.email ??
|
|
1223
|
+
"Unknown guest";
|
|
1224
|
+
const eventDate = meeting?.eventDate ?? "unknown date";
|
|
1225
|
+
const hasTranscript = details?.transcript?.available === true;
|
|
1226
|
+
const hasRecording = !!details?.recordings?.matchedMeetingRecording?.recording;
|
|
1227
|
+
const ready = details?.clipReadiness?.ready === true;
|
|
1228
|
+
const reasons = Array.isArray(details?.clipReadiness?.reasons)
|
|
1229
|
+
? details.clipReadiness.reasons.filter((reason: any) => typeof reason === "string")
|
|
1230
|
+
: [];
|
|
1231
|
+
|
|
1232
|
+
let text = theme.fg(
|
|
1233
|
+
"muted",
|
|
1234
|
+
`${eventDate} · ${guestName} · transcript:${hasTranscript ? "yes" : "no"} · recording:${hasRecording ? "yes" : "no"} · ready:${ready ? "yes" : "no"}`,
|
|
1235
|
+
);
|
|
1236
|
+
if (!ready && reasons.length) {
|
|
1237
|
+
text += theme.fg("dim", `\n blockers: ${reasons.join(", ")}`);
|
|
1238
|
+
}
|
|
1239
|
+
return new Text(text, 0, 0);
|
|
1240
|
+
},
|
|
1195
1241
|
});
|
|
1196
1242
|
|
|
1197
1243
|
pi.registerTool({
|
|
@@ -1214,6 +1260,11 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1214
1260
|
),
|
|
1215
1261
|
}),
|
|
1216
1262
|
execute: wrapExecute(listShowRecordings),
|
|
1263
|
+
renderCall: makeProgressCallRenderer("Checking show recordings", (args) => args?.programSlug || undefined),
|
|
1264
|
+
renderResult: makeProgressResultRenderer("Show recordings loaded", (details) => {
|
|
1265
|
+
const count = Array.isArray(details?.data) ? details.data.length : undefined;
|
|
1266
|
+
return typeof count === "number" ? `${count} recording${count === 1 ? "" : "s"}` : undefined;
|
|
1267
|
+
}),
|
|
1217
1268
|
});
|
|
1218
1269
|
|
|
1219
1270
|
pi.registerTool({
|
|
@@ -1240,22 +1291,34 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1240
1291
|
),
|
|
1241
1292
|
}),
|
|
1242
1293
|
execute: wrapExecute(listMeetingTranscripts),
|
|
1243
|
-
|
|
1294
|
+
renderCall: makeProgressCallRenderer("Checking transcript inventory", (args) => args?.programSlug || undefined),
|
|
1295
|
+
renderResult(result: any, renderArgs: any, theme: any) {
|
|
1244
1296
|
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
1297
|
+
const expanded = renderArgs?.expanded === true;
|
|
1298
|
+
const requestedLimit = Number.isFinite(renderArgs?.limit) ? Math.trunc(renderArgs.limit) : DEFAULT_TRANSCRIPT_LIMIT;
|
|
1245
1299
|
const rows = Array.isArray(result.details?.data) ? result.details.data : [];
|
|
1246
1300
|
const program = result.details?.program?.slug ?? result.details?.program?.name ?? "program";
|
|
1247
1301
|
if (!rows.length) return new Text(theme.fg("dim", `No transcripts found for ${program}`), 0, 0);
|
|
1248
1302
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1303
|
+
const visibleCount = expanded ? 20 : 8;
|
|
1304
|
+
const shown = Math.min(visibleCount, rows.length);
|
|
1305
|
+
let text = theme.fg("muted", `${rows.length} transcript${rows.length === 1 ? "" : "s"} loaded for ${program}`);
|
|
1306
|
+
for (const row of rows.slice(0, shown)) {
|
|
1251
1307
|
const date = row?.transcript?.event_date ?? "unknown date";
|
|
1252
1308
|
const name = row?.transcript_for ?? "Unknown";
|
|
1253
1309
|
const status = row?.transcript?.status ?? "unknown";
|
|
1254
1310
|
text += `\n ${date} - ${name} [${status}]`;
|
|
1255
1311
|
}
|
|
1256
|
-
if (!expanded && rows.length >
|
|
1257
|
-
text += theme.fg("dim", `\n ...and ${rows.length -
|
|
1312
|
+
if (!expanded && rows.length > shown) {
|
|
1313
|
+
text += theme.fg("dim", `\n ...and ${rows.length - shown} more in this response`);
|
|
1258
1314
|
}
|
|
1315
|
+
const mightHaveMore = rows.length >= requestedLimit;
|
|
1316
|
+
text += theme.fg(
|
|
1317
|
+
"dim",
|
|
1318
|
+
`\n Showing ${shown} of ${rows.length} loaded transcript row${rows.length === 1 ? "" : "s"}.${
|
|
1319
|
+
mightHaveMore ? " There may be more available (increase limit up to 20)." : ""
|
|
1320
|
+
}`,
|
|
1321
|
+
);
|
|
1259
1322
|
return new Text(text, 0, 0);
|
|
1260
1323
|
},
|
|
1261
1324
|
});
|
|
@@ -1274,6 +1337,11 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1274
1337
|
),
|
|
1275
1338
|
}),
|
|
1276
1339
|
execute: wrapExecute(getMeetingTranscript),
|
|
1340
|
+
renderCall: makeProgressCallRenderer("Loading meeting transcript", (args) => args?.meetingId || undefined),
|
|
1341
|
+
renderResult: makeProgressResultRenderer("Meeting transcript loaded", (details) => {
|
|
1342
|
+
const date = details?.transcript?.event_date;
|
|
1343
|
+
return typeof date === "string" ? date : undefined;
|
|
1344
|
+
}),
|
|
1277
1345
|
});
|
|
1278
1346
|
|
|
1279
1347
|
pi.registerTool({
|
|
@@ -1285,6 +1353,8 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1285
1353
|
meetingId: Type.String({ description: "Meeting id" }),
|
|
1286
1354
|
}),
|
|
1287
1355
|
execute: wrapExecute(getMeeting),
|
|
1356
|
+
renderCall: makeProgressCallRenderer("Loading meeting details", (args) => args?.meetingId || undefined),
|
|
1357
|
+
renderResult: makeProgressResultRenderer("Meeting details loaded", (details) => details?.meeting?.id || undefined),
|
|
1288
1358
|
});
|
|
1289
1359
|
|
|
1290
1360
|
pi.registerTool({
|
|
@@ -1299,6 +1369,8 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1299
1369
|
producerFraming: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
1300
1370
|
}),
|
|
1301
1371
|
execute: wrapExecute(updateMeeting),
|
|
1372
|
+
renderCall: makeProgressCallRenderer("Updating meeting", (args) => args?.meetingId || undefined),
|
|
1373
|
+
renderResult: makeProgressResultRenderer("Meeting updated"),
|
|
1302
1374
|
});
|
|
1303
1375
|
|
|
1304
1376
|
pi.registerTool({
|
|
@@ -1311,5 +1383,7 @@ export function registerMeetingTools(pi: ExtensionAPI) {
|
|
|
1311
1383
|
syncPendingEmailToPerson: Type.Optional(Type.Boolean({ description: "Whether to sync pending email to the person" })),
|
|
1312
1384
|
}),
|
|
1313
1385
|
execute: wrapExecute(assignMeeting),
|
|
1386
|
+
renderCall: makeProgressCallRenderer("Assigning meeting", (args) => args?.meetingId || undefined),
|
|
1387
|
+
renderResult: makeProgressResultRenderer("Meeting assigned"),
|
|
1314
1388
|
});
|
|
1315
1389
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
3
|
import { ApiError, api } from "../api-client.js";
|
|
4
|
-
import { wrapExecute } from "../tool-utils.js";
|
|
4
|
+
import { makeProgressCallRenderer, makeProgressResultRenderer, wrapExecute } from "../tool-utils.js";
|
|
5
5
|
|
|
6
6
|
export async function getSessionContext() {
|
|
7
7
|
try {
|
|
@@ -98,6 +98,8 @@ export function registerUtilityTools(pi: ExtensionAPI) {
|
|
|
98
98
|
"Get the current Seed Club session, workspace, and accessible program context. Use only for auth, workspace, or program access discovery; do not use for transcript, guest, media, clip, or show-content retrieval.",
|
|
99
99
|
parameters: Type.Object({}),
|
|
100
100
|
execute: wrapExecute(getSessionContext),
|
|
101
|
+
renderCall: makeProgressCallRenderer("Loading workspace session context"),
|
|
102
|
+
renderResult: makeProgressResultRenderer("Workspace context loaded"),
|
|
101
103
|
});
|
|
102
104
|
|
|
103
105
|
pi.registerTool({
|
|
@@ -106,6 +108,8 @@ export function registerUtilityTools(pi: ExtensionAPI) {
|
|
|
106
108
|
description: "Get information about the currently authenticated Seed Club user and their accessible program count.",
|
|
107
109
|
parameters: Type.Object({}),
|
|
108
110
|
execute: wrapExecute(getCurrentUser),
|
|
111
|
+
renderCall: makeProgressCallRenderer("Loading current user"),
|
|
112
|
+
renderResult: makeProgressResultRenderer("Current user loaded", (details) => details?.name || undefined),
|
|
109
113
|
});
|
|
110
114
|
|
|
111
115
|
pi.registerTool({
|
|
@@ -114,6 +118,11 @@ export function registerUtilityTools(pi: ExtensionAPI) {
|
|
|
114
118
|
description: "List the Seed Club programs accessible to the current user.",
|
|
115
119
|
parameters: Type.Object({}),
|
|
116
120
|
execute: wrapExecute(listAccessiblePrograms),
|
|
121
|
+
renderCall: makeProgressCallRenderer("Loading accessible programs"),
|
|
122
|
+
renderResult: makeProgressResultRenderer("Accessible programs loaded", (details) => {
|
|
123
|
+
const count = Array.isArray(details?.programs) ? details.programs.length : 0;
|
|
124
|
+
return `${count} program${count === 1 ? "" : "s"}`;
|
|
125
|
+
}),
|
|
117
126
|
});
|
|
118
127
|
|
|
119
128
|
pi.registerTool({
|
|
@@ -124,5 +133,7 @@ export function registerUtilityTools(pi: ExtensionAPI) {
|
|
|
124
133
|
programSlug: Type.String({ description: "Program slug" }),
|
|
125
134
|
}),
|
|
126
135
|
execute: wrapExecute(getProgramAccess),
|
|
136
|
+
renderCall: makeProgressCallRenderer("Checking program access", (args) => args?.programSlug || undefined),
|
|
137
|
+
renderResult: makeProgressResultRenderer("Program access loaded", (details) => details?.program?.slug || undefined),
|
|
127
138
|
});
|
|
128
139
|
}
|
|
@@ -14,12 +14,13 @@ class SurfaceEditor extends CustomEditor {
|
|
|
14
14
|
private readonly getPalette: () => Palette;
|
|
15
15
|
private static readonly RESET = "\x1b[0m";
|
|
16
16
|
private static readonly INSET = "";
|
|
17
|
+
private static readonly CONTENT_INSET = 2;
|
|
17
18
|
private static readonly PLACEHOLDER = "/ for menu";
|
|
18
19
|
|
|
19
20
|
constructor(tui: TUI, theme: EditorTheme, kb: KeybindingsManager, getPalette: () => Palette) {
|
|
20
21
|
super(tui, theme, kb);
|
|
21
22
|
this.getPalette = getPalette;
|
|
22
|
-
|
|
23
|
+
super.setPaddingX(SurfaceEditor.CONTENT_INSET);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
render(width: number): string[] {
|
|
@@ -47,6 +48,10 @@ class SurfaceEditor extends CustomEditor {
|
|
|
47
48
|
super.invalidate();
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
override setPaddingX(_padding: number): void {
|
|
52
|
+
super.setPaddingX(SurfaceEditor.CONTENT_INSET);
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
override handleInput(data: string): void {
|
|
51
56
|
if (!uiState.ready) {
|
|
52
57
|
// Keep app-level interrupts available while startup UI is loading.
|
|
@@ -8,10 +8,12 @@ import editorExtension from "./editor.js";
|
|
|
8
8
|
import footerExtension from "./footer.js";
|
|
9
9
|
import updateExtension from "./update.js";
|
|
10
10
|
import welcomeExtension from "./welcome.js";
|
|
11
|
+
import toolProgressExtension from "./tool-progress.js";
|
|
11
12
|
|
|
12
13
|
export default function (pi: ExtensionAPI) {
|
|
13
14
|
editorExtension(pi);
|
|
14
15
|
footerExtension(pi);
|
|
15
16
|
updateExtension(pi);
|
|
17
|
+
toolProgressExtension(pi);
|
|
16
18
|
welcomeExtension(pi, { enableFrame: false });
|
|
17
19
|
}
|