@alasano/pi-linear 0.1.0 → 0.2.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.
Files changed (36) hide show
  1. package/README.md +14 -2
  2. package/assets/linear_list_issues.png +0 -0
  3. package/assets/screenshot.png +0 -0
  4. package/extensions/index.ts +1 -1
  5. package/extensions/renderers/comments.ts +323 -0
  6. package/extensions/renderers/common.ts +305 -0
  7. package/extensions/renderers/documents.ts +326 -0
  8. package/extensions/renderers/initiatives.ts +344 -0
  9. package/extensions/renderers/issue-labels.ts +294 -0
  10. package/extensions/renderers/issue-relations.ts +318 -0
  11. package/extensions/renderers/issue-statuses.ts +199 -0
  12. package/extensions/renderers/issues.ts +373 -0
  13. package/extensions/renderers/milestones.ts +294 -0
  14. package/extensions/renderers/project-labels.ts +279 -0
  15. package/extensions/renderers/project-relations.ts +344 -0
  16. package/extensions/renderers/projects.ts +430 -0
  17. package/extensions/renderers/state.ts +35 -0
  18. package/extensions/renderers/teams.ts +246 -0
  19. package/extensions/renderers/users.ts +242 -0
  20. package/extensions/renderers/workspaces.ts +44 -0
  21. package/extensions/settings.ts +53 -23
  22. package/extensions/tools/comments.ts +17 -0
  23. package/extensions/tools/documents.ts +23 -0
  24. package/extensions/tools/initiatives.ts +24 -0
  25. package/extensions/tools/issue-labels.ts +17 -0
  26. package/extensions/tools/issue-relations.ts +29 -5
  27. package/extensions/tools/issue-statuses.ts +6 -0
  28. package/extensions/tools/issues.ts +29 -0
  29. package/extensions/tools/milestones.ts +18 -0
  30. package/extensions/tools/project-labels.ts +17 -0
  31. package/extensions/tools/project-relations.ts +17 -0
  32. package/extensions/tools/projects.ts +25 -1
  33. package/extensions/tools/teams.ts +11 -3
  34. package/extensions/tools/users.ts +10 -0
  35. package/extensions/tools/workspaces.ts +6 -0
  36. package/package.json +1 -1
@@ -0,0 +1,294 @@
1
+ import {
2
+ type AgentToolResult,
3
+ type Theme,
4
+ type ToolRenderResultOptions,
5
+ } from '@mariozechner/pi-coding-agent';
6
+ import { Text } from '@mariozechner/pi-tui';
7
+ import {
8
+ accentStyle,
9
+ asString,
10
+ cleanOneLine,
11
+ dimStyle,
12
+ expandedJson,
13
+ shouldShowJson,
14
+ jsonHint,
15
+ LinearListResultComponent,
16
+ mutedStyle,
17
+ renderLinearToolCall,
18
+ renderResponsiveTable,
19
+ toolOutputStyle,
20
+ truncate,
21
+ truncateLine,
22
+ type LinearToolRenderContext,
23
+ type TableColumn,
24
+ type ToolArgs,
25
+ } from './common';
26
+
27
+ type MilestoneLike = {
28
+ id?: string | null;
29
+ name?: string | null;
30
+ description?: string | null;
31
+ status?: string | null;
32
+ progress?: number | null;
33
+ targetDate?: string | null;
34
+ sortOrder?: number | null;
35
+ createdAt?: string | null;
36
+ updatedAt?: string | null;
37
+ project?: {
38
+ id?: string | null;
39
+ name?: string | null;
40
+ url?: string | null;
41
+ } | null;
42
+ };
43
+
44
+ type MilestoneResultDetails = {
45
+ milestone?: MilestoneLike | null;
46
+ milestones?: MilestoneLike[];
47
+ success?: boolean;
48
+ };
49
+
50
+ const MILESTONE_LIST_PREVIEW_LIMIT = 20;
51
+ const NAME_LIMIT = 90;
52
+ const DESCRIPTION_LIMIT = 180;
53
+ const TABLE_NAME_MIN_WIDTH = 24;
54
+
55
+ function milestoneDetails(result: AgentToolResult<any>): MilestoneResultDetails {
56
+ return (result.details ?? {}) as MilestoneResultDetails;
57
+ }
58
+
59
+ function milestoneName(milestone: MilestoneLike): string {
60
+ return truncate(cleanOneLine(asString(milestone.name) ?? '(unnamed milestone)'), NAME_LIMIT);
61
+ }
62
+
63
+ function milestoneProject(milestone: MilestoneLike): string | undefined {
64
+ return asString(milestone.project?.name) ?? asString(milestone.project?.id);
65
+ }
66
+
67
+ function milestoneStatus(milestone: MilestoneLike): string | undefined {
68
+ return asString(milestone.status);
69
+ }
70
+
71
+ function milestoneTarget(milestone: MilestoneLike): string | undefined {
72
+ return asString(milestone.targetDate);
73
+ }
74
+
75
+ function milestoneProgress(milestone: MilestoneLike): string | undefined {
76
+ const progress = milestone.progress;
77
+ if (typeof progress !== 'number' || !Number.isFinite(progress)) return undefined;
78
+
79
+ const percent = progress >= 0 && progress <= 1 ? progress * 100 : progress;
80
+ return `${Math.round(percent)}%`;
81
+ }
82
+
83
+ function metadataParts(milestone: MilestoneLike): string[] {
84
+ const project = milestoneProject(milestone);
85
+ const status = milestoneStatus(milestone);
86
+ const progress = milestoneProgress(milestone);
87
+ const target = milestoneTarget(milestone);
88
+
89
+ return [
90
+ project ? `project: ${project}` : undefined,
91
+ status ? `status: ${status}` : undefined,
92
+ progress ? `progress: ${progress}` : undefined,
93
+ target ? `target: ${target}` : undefined,
94
+ ].filter((part): part is string => !!part);
95
+ }
96
+
97
+ function descriptionSnippet(milestone: MilestoneLike): string | undefined {
98
+ const description = asString(milestone.description);
99
+ if (!description) return undefined;
100
+ return truncate(cleanOneLine(description), DESCRIPTION_LIMIT);
101
+ }
102
+
103
+ function formatMilestoneListLine(milestone: MilestoneLike, theme: Theme, width: number): string {
104
+ const name = milestoneName(milestone);
105
+ const metadata = metadataParts(milestone);
106
+ const suffix = metadata.length ? theme.fg('dim', ` · ${metadata.join(' · ')}`) : '';
107
+
108
+ return truncateLine(` ${theme.fg('toolOutput', name)}${suffix}`, width);
109
+ }
110
+
111
+ function formatMilestoneTitle(milestone: MilestoneLike, theme: Theme): string {
112
+ const id = asString(milestone.id);
113
+ const name = theme.fg('toolOutput', milestoneName(milestone));
114
+ return id ? `${name} ${theme.fg('dim', `(${truncate(id, 8)})`)}` : name;
115
+ }
116
+
117
+ function statusStyle(theme: Theme, value: string): (text: string) => string {
118
+ const normalized = value.toLowerCase();
119
+ if (normalized === 'completed' || normalized === 'done')
120
+ return (text) => theme.fg('success', text);
121
+ if (normalized === 'canceled' || normalized === 'cancelled')
122
+ return (text) => theme.fg('error', text);
123
+ if (normalized === 'planned' || value === '—') return dimStyle(theme);
124
+ return mutedStyle(theme);
125
+ }
126
+
127
+ const MILESTONE_TABLE_COLUMNS: TableColumn<MilestoneLike>[] = [
128
+ {
129
+ id: 'project',
130
+ label: 'Project',
131
+ width: 20,
132
+ value: (milestone) => milestoneProject(milestone) ?? '—',
133
+ style: (theme) => accentStyle(theme),
134
+ },
135
+ {
136
+ id: 'status',
137
+ label: 'Status',
138
+ width: 12,
139
+ value: (milestone) => milestoneStatus(milestone) ?? '—',
140
+ style: statusStyle,
141
+ },
142
+ {
143
+ id: 'progress',
144
+ label: 'Progress',
145
+ width: 9,
146
+ value: (milestone) => milestoneProgress(milestone) ?? '—',
147
+ style: (theme) => mutedStyle(theme),
148
+ },
149
+ {
150
+ id: 'target',
151
+ label: 'Target',
152
+ width: 12,
153
+ value: (milestone) => milestoneTarget(milestone) ?? '—',
154
+ style: (theme) => dimStyle(theme),
155
+ },
156
+ ];
157
+
158
+ function renderMilestoneTable(milestones: MilestoneLike[], theme: Theme, width: number): string[] {
159
+ return renderResponsiveTable(milestones, theme, width, {
160
+ columns: MILESTONE_TABLE_COLUMNS,
161
+ primary: {
162
+ label: 'Name',
163
+ minWidth: TABLE_NAME_MIN_WIDTH,
164
+ value: milestoneName,
165
+ style: (theme) => toolOutputStyle(theme),
166
+ },
167
+ dropOrder: ['progress', 'target', 'status', 'project'],
168
+ fallback: formatMilestoneListLine,
169
+ });
170
+ }
171
+
172
+ function renderMilestoneCard(
173
+ result: AgentToolResult<any>,
174
+ options: ToolRenderResultOptions,
175
+ theme: Theme,
176
+ context: LinearToolRenderContext,
177
+ actionLabel: string,
178
+ ): Text {
179
+ if (options.isPartial) return new Text(theme.fg('warning', `${actionLabel}…`), 0, 0);
180
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
181
+
182
+ const milestone = milestoneDetails(result).milestone;
183
+ if (!milestone) {
184
+ return new Text(`\n${theme.fg('dim', 'Milestone not found')}\n\n${jsonHint()}`, 0, 0);
185
+ }
186
+
187
+ const metadata = metadataParts(milestone);
188
+ const description = descriptionSnippet(milestone);
189
+
190
+ let text = `\n${theme.fg('success', `✓ ${actionLabel}`)} ${formatMilestoneTitle(milestone, theme)}`;
191
+ if (metadata.length) text += `\n ${theme.fg('dim', metadata.join(' · '))}`;
192
+ if (description) text += `\n ${theme.fg('muted', description)}`;
193
+ text += `\n\n${jsonHint()}`;
194
+
195
+ return new Text(text, 0, 0);
196
+ }
197
+
198
+ export function renderLinearMilestoneListCall(args: ToolArgs | undefined, theme: Theme): Text {
199
+ return renderLinearToolCall('linear_list_milestones', args, theme, [
200
+ ['first', 'first'],
201
+ ['last', 'last'],
202
+ ['orderBy', 'order'],
203
+ ['includeArchived', 'archived'],
204
+ ['filter', 'filter'],
205
+ ]);
206
+ }
207
+
208
+ export function renderLinearMilestoneGetCall(args: ToolArgs | undefined, theme: Theme): Text {
209
+ return renderLinearToolCall('linear_get_milestone', args, theme, [
210
+ ['milestoneId', 'milestoneId'],
211
+ ]);
212
+ }
213
+
214
+ export function renderLinearMilestoneSaveCall(args: ToolArgs | undefined, theme: Theme): Text {
215
+ return renderLinearToolCall('linear_save_milestone', args, theme, [
216
+ ['milestoneId', 'milestoneId'],
217
+ ['name', 'name'],
218
+ ['projectId', 'projectId'],
219
+ ['targetDate', 'target'],
220
+ ]);
221
+ }
222
+
223
+ export function renderLinearMilestoneDeleteCall(args: ToolArgs | undefined, theme: Theme): Text {
224
+ return renderLinearToolCall('linear_delete_milestone', args, theme, [
225
+ ['milestoneId', 'milestoneId'],
226
+ ]);
227
+ }
228
+
229
+ export function renderLinearMilestoneListResult(
230
+ result: AgentToolResult<any>,
231
+ options: ToolRenderResultOptions,
232
+ theme: Theme,
233
+ context: LinearToolRenderContext,
234
+ ): Text | LinearListResultComponent<MilestoneLike> {
235
+ if (options.isPartial) return new Text(theme.fg('warning', 'Loading milestones…'), 0, 0);
236
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
237
+
238
+ const milestones = Array.isArray(milestoneDetails(result).milestones)
239
+ ? (milestoneDetails(result).milestones as MilestoneLike[])
240
+ : [];
241
+
242
+ return new LinearListResultComponent(milestones, theme, {
243
+ noun: 'milestone',
244
+ emptyLabel: 'No milestones found',
245
+ previewLimit: MILESTONE_LIST_PREVIEW_LIMIT,
246
+ renderItems: renderMilestoneTable,
247
+ });
248
+ }
249
+
250
+ export function renderLinearMilestoneResult(actionLabel: string) {
251
+ return (
252
+ result: AgentToolResult<any>,
253
+ options: ToolRenderResultOptions,
254
+ theme: Theme,
255
+ context: LinearToolRenderContext,
256
+ ): Text => renderMilestoneCard(result, options, theme, context, actionLabel);
257
+ }
258
+
259
+ export function renderLinearMilestoneSaveResult() {
260
+ return (
261
+ result: AgentToolResult<any>,
262
+ options: ToolRenderResultOptions,
263
+ theme: Theme,
264
+ context: { args?: unknown },
265
+ ): Text => {
266
+ const args = (context.args ?? {}) as { milestoneId?: unknown };
267
+ const actionLabel = asString(args.milestoneId) ? 'Updated milestone' : 'Created milestone';
268
+ return renderMilestoneCard(result, options, theme, context, actionLabel);
269
+ };
270
+ }
271
+
272
+ export function renderLinearMilestoneDeleteResult(
273
+ result: AgentToolResult<any>,
274
+ options: ToolRenderResultOptions,
275
+ theme: Theme,
276
+ context: { args?: unknown },
277
+ ): Text {
278
+ if (options.isPartial) return new Text(theme.fg('warning', 'Deleting milestone…'), 0, 0);
279
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
280
+
281
+ const details = milestoneDetails(result);
282
+ const args = (context.args ?? {}) as { milestoneId?: unknown };
283
+ const milestoneId = asString(args.milestoneId) ?? 'milestone';
284
+
285
+ if (details.success !== true) {
286
+ return new Text(`\n${theme.fg('warning', 'Delete status unknown')}\n\n${jsonHint()}`, 0, 0);
287
+ }
288
+
289
+ return new Text(
290
+ `\n${theme.fg('success', '✓ Deleted milestone')} ${theme.fg('accent', milestoneId)}\n\n${jsonHint()}`,
291
+ 0,
292
+ 0,
293
+ );
294
+ }
@@ -0,0 +1,279 @@
1
+ import {
2
+ type AgentToolResult,
3
+ type Theme,
4
+ type ToolRenderResultOptions,
5
+ } from '@mariozechner/pi-coding-agent';
6
+ import { Text } from '@mariozechner/pi-tui';
7
+ import {
8
+ accentStyle,
9
+ asString,
10
+ cleanOneLine,
11
+ dimStyle,
12
+ expandedJson,
13
+ shouldShowJson,
14
+ jsonHint,
15
+ LinearListResultComponent,
16
+ mutedStyle,
17
+ renderLinearToolCall,
18
+ renderResponsiveTable,
19
+ toolOutputStyle,
20
+ truncate,
21
+ truncateLine,
22
+ type LinearToolRenderContext,
23
+ type TableColumn,
24
+ type ToolArgs,
25
+ } from './common';
26
+
27
+ type ProjectLabelLike = {
28
+ id?: string | null;
29
+ name?: string | null;
30
+ description?: string | null;
31
+ color?: string | null;
32
+ isGroup?: boolean | null;
33
+ createdAt?: string | null;
34
+ updatedAt?: string | null;
35
+ retiredAt?: string | null;
36
+ parent?: {
37
+ id?: string | null;
38
+ name?: string | null;
39
+ } | null;
40
+ };
41
+
42
+ type ProjectLabelResultDetails = {
43
+ label?: ProjectLabelLike | null;
44
+ labels?: ProjectLabelLike[];
45
+ success?: boolean;
46
+ };
47
+
48
+ const PROJECT_LABEL_LIST_PREVIEW_LIMIT = 20;
49
+ const NAME_LIMIT = 90;
50
+ const DESCRIPTION_LIMIT = 180;
51
+ const TABLE_NAME_MIN_WIDTH = 24;
52
+
53
+ function projectLabelDetails(result: AgentToolResult<any>): ProjectLabelResultDetails {
54
+ return (result.details ?? {}) as ProjectLabelResultDetails;
55
+ }
56
+
57
+ function labelName(label: ProjectLabelLike): string {
58
+ return truncate(cleanOneLine(asString(label.name) ?? '(unnamed label)'), NAME_LIMIT);
59
+ }
60
+
61
+ function parentName(label: ProjectLabelLike): string | undefined {
62
+ return asString(label.parent?.name) ?? asString(label.parent?.id);
63
+ }
64
+
65
+ function groupText(label: ProjectLabelLike): string | undefined {
66
+ return label.isGroup === true ? 'group' : undefined;
67
+ }
68
+
69
+ function retiredText(label: ProjectLabelLike): string | undefined {
70
+ return asString(label.retiredAt) ? 'retired' : undefined;
71
+ }
72
+
73
+ function colorText(label: ProjectLabelLike): string | undefined {
74
+ return asString(label.color);
75
+ }
76
+
77
+ function descriptionSnippet(label: ProjectLabelLike): string | undefined {
78
+ const description = asString(label.description);
79
+ if (!description) return undefined;
80
+ return truncate(cleanOneLine(description), DESCRIPTION_LIMIT);
81
+ }
82
+
83
+ function metadataParts(
84
+ label: ProjectLabelLike,
85
+ options: { includeDescription?: boolean } = {},
86
+ ): string[] {
87
+ const parent = parentName(label);
88
+ const group = groupText(label);
89
+ const retired = retiredText(label);
90
+ const color = colorText(label);
91
+ const description = options.includeDescription ? descriptionSnippet(label) : undefined;
92
+
93
+ return [
94
+ parent ? `parent: ${parent}` : undefined,
95
+ group,
96
+ retired,
97
+ color ? `color: ${color}` : undefined,
98
+ description,
99
+ ].filter((part): part is string => !!part);
100
+ }
101
+
102
+ function formatProjectLabelListLine(label: ProjectLabelLike, theme: Theme, width: number): string {
103
+ const name = labelName(label);
104
+ const metadata = metadataParts(label, { includeDescription: true });
105
+ const suffix = metadata.length ? theme.fg('dim', ` · ${metadata.join(' · ')}`) : '';
106
+
107
+ return truncateLine(` ${theme.fg('toolOutput', name)}${suffix}`, width);
108
+ }
109
+
110
+ function formatProjectLabelTitle(label: ProjectLabelLike, theme: Theme): string {
111
+ const id = asString(label.id);
112
+ const name = theme.fg('toolOutput', labelName(label));
113
+ return id ? `${name} ${theme.fg('dim', `(${truncate(id, 8)})`)}` : name;
114
+ }
115
+
116
+ function groupColumnText(label: ProjectLabelLike): string {
117
+ return label.isGroup === true ? 'yes' : '—';
118
+ }
119
+
120
+ function colorStyle(theme: Theme, value: string): (text: string) => string {
121
+ if (value === '—') return dimStyle(theme);
122
+ return accentStyle(theme);
123
+ }
124
+
125
+ const PROJECT_LABEL_TABLE_COLUMNS: TableColumn<ProjectLabelLike>[] = [
126
+ {
127
+ id: 'parent',
128
+ label: 'Parent',
129
+ width: 20,
130
+ value: (label) => parentName(label) ?? '—',
131
+ style: (theme) => mutedStyle(theme),
132
+ },
133
+ {
134
+ id: 'group',
135
+ label: 'Group',
136
+ width: 7,
137
+ value: groupColumnText,
138
+ style: (theme, value) => (value === 'yes' ? mutedStyle(theme) : dimStyle(theme)),
139
+ },
140
+ {
141
+ id: 'color',
142
+ label: 'Color',
143
+ width: 10,
144
+ value: (label) => colorText(label) ?? '—',
145
+ style: colorStyle,
146
+ },
147
+ ];
148
+
149
+ function renderProjectLabelTable(
150
+ labels: ProjectLabelLike[],
151
+ theme: Theme,
152
+ width: number,
153
+ ): string[] {
154
+ return renderResponsiveTable(labels, theme, width, {
155
+ columns: PROJECT_LABEL_TABLE_COLUMNS,
156
+ primary: {
157
+ label: 'Name',
158
+ minWidth: TABLE_NAME_MIN_WIDTH,
159
+ value: labelName,
160
+ style: (theme) => toolOutputStyle(theme),
161
+ },
162
+ dropOrder: ['color', 'group', 'parent'],
163
+ fallback: formatProjectLabelListLine,
164
+ });
165
+ }
166
+
167
+ function renderProjectLabelCard(
168
+ result: AgentToolResult<any>,
169
+ options: ToolRenderResultOptions,
170
+ theme: Theme,
171
+ context: LinearToolRenderContext,
172
+ actionLabel: string,
173
+ ): Text {
174
+ if (options.isPartial) return new Text(theme.fg('warning', `${actionLabel}…`), 0, 0);
175
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
176
+
177
+ const label = projectLabelDetails(result).label;
178
+ if (!label) {
179
+ return new Text(`\n${theme.fg('dim', 'Project label not found')}\n\n${jsonHint()}`, 0, 0);
180
+ }
181
+
182
+ const metadata = metadataParts(label);
183
+ const description = descriptionSnippet(label);
184
+
185
+ let text = `\n${theme.fg('success', `✓ ${actionLabel}`)} ${formatProjectLabelTitle(label, theme)}`;
186
+ if (metadata.length) text += `\n ${theme.fg('dim', metadata.join(' · '))}`;
187
+ if (description) text += `\n ${theme.fg('muted', description)}`;
188
+ text += `\n\n${jsonHint()}`;
189
+
190
+ return new Text(text, 0, 0);
191
+ }
192
+
193
+ export function renderLinearProjectLabelListCall(args: ToolArgs | undefined, theme: Theme): Text {
194
+ return renderLinearToolCall('linear_list_project_labels', args, theme, [
195
+ ['first', 'first'],
196
+ ['last', 'last'],
197
+ ['orderBy', 'order'],
198
+ ['includeArchived', 'archived'],
199
+ ['filter', 'filter'],
200
+ ]);
201
+ }
202
+
203
+ export function renderLinearCreateProjectLabelCall(args: ToolArgs | undefined, theme: Theme): Text {
204
+ return renderLinearToolCall('linear_create_project_label', args, theme, [
205
+ ['id', 'id'],
206
+ ['name', 'name'],
207
+ ['parentId', 'parentId'],
208
+ ['color', 'color'],
209
+ ['isGroup', 'isGroup'],
210
+ ]);
211
+ }
212
+
213
+ export function renderLinearUpdateProjectLabelCall(args: ToolArgs | undefined, theme: Theme): Text {
214
+ return renderLinearToolCall('linear_update_project_label', args, theme, [
215
+ ['id', 'id'],
216
+ ['name', 'name'],
217
+ ['parentId', 'parentId'],
218
+ ['color', 'color'],
219
+ ['isGroup', 'isGroup'],
220
+ ]);
221
+ }
222
+
223
+ export function renderLinearDeleteProjectLabelCall(args: ToolArgs | undefined, theme: Theme): Text {
224
+ return renderLinearToolCall('linear_delete_project_label', args, theme, [['id', 'id']]);
225
+ }
226
+
227
+ export function renderLinearProjectLabelListResult(
228
+ result: AgentToolResult<any>,
229
+ options: ToolRenderResultOptions,
230
+ theme: Theme,
231
+ context: LinearToolRenderContext,
232
+ ): Text | LinearListResultComponent<ProjectLabelLike> {
233
+ if (options.isPartial) return new Text(theme.fg('warning', 'Loading project labels…'), 0, 0);
234
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
235
+
236
+ const labels = Array.isArray(projectLabelDetails(result).labels)
237
+ ? (projectLabelDetails(result).labels as ProjectLabelLike[])
238
+ : [];
239
+
240
+ return new LinearListResultComponent(labels, theme, {
241
+ noun: 'label',
242
+ emptyLabel: 'No project labels found',
243
+ previewLimit: PROJECT_LABEL_LIST_PREVIEW_LIMIT,
244
+ renderItems: renderProjectLabelTable,
245
+ });
246
+ }
247
+
248
+ export function renderLinearProjectLabelResult(actionLabel: string) {
249
+ return (
250
+ result: AgentToolResult<any>,
251
+ options: ToolRenderResultOptions,
252
+ theme: Theme,
253
+ context: LinearToolRenderContext,
254
+ ): Text => renderProjectLabelCard(result, options, theme, context, actionLabel);
255
+ }
256
+
257
+ export function renderLinearProjectLabelDeleteResult(
258
+ result: AgentToolResult<any>,
259
+ options: ToolRenderResultOptions,
260
+ theme: Theme,
261
+ context: { args?: unknown },
262
+ ): Text {
263
+ if (options.isPartial) return new Text(theme.fg('warning', 'Deleting project label…'), 0, 0);
264
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
265
+
266
+ const details = projectLabelDetails(result);
267
+ const args = (context.args ?? {}) as { id?: unknown };
268
+ const id = asString(args.id) ?? 'project label';
269
+
270
+ if (details.success !== true) {
271
+ return new Text(`\n${theme.fg('warning', 'Delete status unknown')}\n\n${jsonHint()}`, 0, 0);
272
+ }
273
+
274
+ return new Text(
275
+ `\n${theme.fg('success', '✓ Deleted project label')} ${theme.fg('accent', id)}\n\n${jsonHint()}`,
276
+ 0,
277
+ 0,
278
+ );
279
+ }