@alasano/pi-linear 0.1.1 → 0.3.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 (38) hide show
  1. package/README.md +16 -2
  2. package/assets/linear_list_issues.png +0 -0
  3. package/assets/screenshot.png +0 -0
  4. package/extensions/params.ts +40 -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 +437 -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/selections.ts +10 -3
  22. package/extensions/settings.ts +40 -7
  23. package/extensions/tools/comments.ts +30 -11
  24. package/extensions/tools/documents.ts +42 -11
  25. package/extensions/tools/initiatives.ts +43 -11
  26. package/extensions/tools/issue-labels.ts +36 -11
  27. package/extensions/tools/issue-relations.ts +32 -13
  28. package/extensions/tools/issue-statuses.ts +19 -11
  29. package/extensions/tools/issues.ts +53 -19
  30. package/extensions/tools/milestones.ts +31 -11
  31. package/extensions/tools/project-labels.ts +30 -11
  32. package/extensions/tools/project-relations.ts +32 -13
  33. package/extensions/tools/projects.ts +48 -16
  34. package/extensions/tools/teams.ts +23 -11
  35. package/extensions/tools/users.ts +23 -11
  36. package/extensions/tools/workspaces.ts +6 -0
  37. package/extensions/types.ts +12 -0
  38. package/package.json +1 -1
@@ -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
+ }
@@ -0,0 +1,344 @@
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
+ asString,
9
+ cleanOneLine,
10
+ dimStyle,
11
+ expandedJson,
12
+ shouldShowJson,
13
+ jsonHint,
14
+ LinearListResultComponent,
15
+ mutedStyle,
16
+ renderLinearToolCall,
17
+ renderResponsiveTable,
18
+ toolOutputStyle,
19
+ truncate,
20
+ truncateLine,
21
+ type LinearToolRenderContext,
22
+ type TableColumn,
23
+ type ToolArgs,
24
+ } from './common';
25
+
26
+ type ProjectRelationProject = {
27
+ id?: string | null;
28
+ name?: string | null;
29
+ };
30
+
31
+ type ProjectRelationMilestone = {
32
+ id?: string | null;
33
+ name?: string | null;
34
+ };
35
+
36
+ type ProjectRelationLike = {
37
+ id?: string | null;
38
+ createdAt?: string | null;
39
+ updatedAt?: string | null;
40
+ type?: string | null;
41
+ anchorType?: string | null;
42
+ relatedAnchorType?: string | null;
43
+ project?: ProjectRelationProject | null;
44
+ projectMilestone?: ProjectRelationMilestone | null;
45
+ relatedProject?: ProjectRelationProject | null;
46
+ relatedProjectMilestone?: ProjectRelationMilestone | null;
47
+ };
48
+
49
+ type ProjectRelationResultDetails = {
50
+ projectRelation?: ProjectRelationLike | null;
51
+ projectRelations?: ProjectRelationLike[];
52
+ success?: boolean;
53
+ };
54
+
55
+ const PROJECT_RELATION_LIST_PREVIEW_LIMIT = 20;
56
+ const NAME_LIMIT = 70;
57
+ const TABLE_RELATION_MIN_WIDTH = 28;
58
+
59
+ const PROJECT_RELATION_MUTATION_FIELDS: Array<[key: string, label: string]> = [
60
+ ['id', 'id'],
61
+ ['projectId', 'projectId'],
62
+ ['relatedProjectId', 'relatedProjectId'],
63
+ ['type', 'type'],
64
+ ['anchorType', 'anchor'],
65
+ ['relatedAnchorType', 'relatedAnchor'],
66
+ ['projectMilestoneId', 'projectMilestone'],
67
+ ['relatedProjectMilestoneId', 'relatedProjectMilestone'],
68
+ ];
69
+
70
+ function projectRelationDetails(result: AgentToolResult<any>): ProjectRelationResultDetails {
71
+ return (result.details ?? {}) as ProjectRelationResultDetails;
72
+ }
73
+
74
+ function argsObject(context: { args?: unknown }): ToolArgs {
75
+ return context.args && typeof context.args === 'object' && !Array.isArray(context.args)
76
+ ? (context.args as ToolArgs)
77
+ : {};
78
+ }
79
+
80
+ function projectName(project: ProjectRelationProject | null | undefined, fallback: string): string {
81
+ return truncate(
82
+ cleanOneLine(asString(project?.name) ?? asString(project?.id) ?? fallback),
83
+ NAME_LIMIT,
84
+ );
85
+ }
86
+
87
+ function hasProject(project: ProjectRelationProject | null | undefined): boolean {
88
+ return !!(asString(project?.name) ?? asString(project?.id));
89
+ }
90
+
91
+ function relationType(relation: ProjectRelationLike): string {
92
+ return asString(relation.type) ?? 'relation';
93
+ }
94
+
95
+ function milestoneName(milestone: ProjectRelationMilestone | null | undefined): string | undefined {
96
+ const name = asString(milestone?.name) ?? asString(milestone?.id);
97
+ return name ? truncate(cleanOneLine(name), NAME_LIMIT) : undefined;
98
+ }
99
+
100
+ function anchorText(
101
+ anchorType: string | null | undefined,
102
+ milestone: ProjectRelationMilestone | null | undefined,
103
+ ): string | undefined {
104
+ const anchor = asString(anchorType);
105
+ const milestoneLabel = milestoneName(milestone);
106
+
107
+ if (anchor && milestoneLabel) return `${anchor}: ${milestoneLabel}`;
108
+ return milestoneLabel ?? anchor;
109
+ }
110
+
111
+ function projectAnchorText(relation: ProjectRelationLike): string | undefined {
112
+ return anchorText(relation.anchorType, relation.projectMilestone);
113
+ }
114
+
115
+ function relatedProjectAnchorText(relation: ProjectRelationLike): string | undefined {
116
+ return anchorText(relation.relatedAnchorType, relation.relatedProjectMilestone);
117
+ }
118
+
119
+ function relationSummary(relation: ProjectRelationLike): string {
120
+ const id = asString(relation.id);
121
+ const hasSource = hasProject(relation.project);
122
+ const hasRelated = hasProject(relation.relatedProject);
123
+
124
+ if (!hasSource && !hasRelated && id) return truncate(id, NAME_LIMIT);
125
+
126
+ const project = projectName(relation.project, 'project');
127
+ const relatedProject = projectName(relation.relatedProject, 'related project');
128
+ return truncate(
129
+ cleanOneLine(`${project} ${relationType(relation)} ${relatedProject}`),
130
+ NAME_LIMIT * 2,
131
+ );
132
+ }
133
+
134
+ function metadataParts(
135
+ relation: ProjectRelationLike,
136
+ options: { includeId?: boolean } = {},
137
+ ): string[] {
138
+ const anchor = projectAnchorText(relation);
139
+ const relatedAnchor = relatedProjectAnchorText(relation);
140
+ const id = options.includeId ? asString(relation.id) : undefined;
141
+
142
+ return [
143
+ anchor ? `anchor: ${anchor}` : undefined,
144
+ relatedAnchor ? `related anchor: ${relatedAnchor}` : undefined,
145
+ id ? `id: ${truncate(id, 8)}` : undefined,
146
+ ].filter((part): part is string => !!part);
147
+ }
148
+
149
+ function formatProjectRelationListLine(
150
+ relation: ProjectRelationLike,
151
+ theme: Theme,
152
+ width: number,
153
+ ): string {
154
+ const summary = relationSummary(relation);
155
+ const metadata = metadataParts(relation, { includeId: true });
156
+ const suffix = metadata.length ? theme.fg('dim', ` · ${metadata.join(' · ')}`) : '';
157
+
158
+ return truncateLine(` ${theme.fg('toolOutput', summary)}${suffix}`, width);
159
+ }
160
+
161
+ const PROJECT_RELATION_TABLE_COLUMNS: TableColumn<ProjectRelationLike>[] = [
162
+ {
163
+ id: 'type',
164
+ label: 'Type',
165
+ width: 12,
166
+ value: (relation) => relationType(relation),
167
+ style: (theme) => mutedStyle(theme),
168
+ },
169
+ {
170
+ id: 'project',
171
+ label: 'Project',
172
+ width: 22,
173
+ value: (relation) => projectName(relation.project, '—'),
174
+ style: (theme) => toolOutputStyle(theme),
175
+ },
176
+ {
177
+ id: 'relatedProject',
178
+ label: 'Related project',
179
+ width: 22,
180
+ value: (relation) => projectName(relation.relatedProject, '—'),
181
+ style: (theme) => toolOutputStyle(theme),
182
+ },
183
+ {
184
+ id: 'anchor',
185
+ label: 'Anchor',
186
+ width: 20,
187
+ value: (relation) => projectAnchorText(relation) ?? '—',
188
+ style: (theme) => dimStyle(theme),
189
+ },
190
+ {
191
+ id: 'relatedAnchor',
192
+ label: 'Related anchor',
193
+ width: 20,
194
+ value: (relation) => relatedProjectAnchorText(relation) ?? '—',
195
+ style: (theme) => dimStyle(theme),
196
+ },
197
+ ];
198
+
199
+ function renderProjectRelationTable(
200
+ projectRelations: ProjectRelationLike[],
201
+ theme: Theme,
202
+ width: number,
203
+ ): string[] {
204
+ return renderResponsiveTable(projectRelations, theme, width, {
205
+ columns: PROJECT_RELATION_TABLE_COLUMNS,
206
+ primary: {
207
+ label: 'Relation',
208
+ minWidth: TABLE_RELATION_MIN_WIDTH,
209
+ value: relationSummary,
210
+ style: (theme) => toolOutputStyle(theme),
211
+ },
212
+ dropOrder: ['relatedAnchor', 'anchor', 'relatedProject', 'project', 'type'],
213
+ fallback: formatProjectRelationListLine,
214
+ });
215
+ }
216
+
217
+ function formatProjectRelationTitle(relation: ProjectRelationLike, theme: Theme): string {
218
+ const id = asString(relation.id);
219
+ const title = theme.fg('toolOutput', relationSummary(relation));
220
+ return id ? `${title} ${theme.fg('dim', `(${truncate(id, 8)})`)}` : title;
221
+ }
222
+
223
+ function renderProjectRelationCard(
224
+ actionLabel: string,
225
+ relation: ProjectRelationLike | null | undefined,
226
+ theme: Theme,
227
+ ): Text {
228
+ if (!relation) {
229
+ return new Text(`\n${theme.fg('dim', 'Project relation not found')}\n\n${jsonHint()}`, 0, 0);
230
+ }
231
+
232
+ const metadata = metadataParts(relation);
233
+
234
+ let text = `\n${theme.fg('success', `✓ ${actionLabel}`)} ${formatProjectRelationTitle(relation, theme)}`;
235
+ if (metadata.length) text += `\n ${theme.fg('dim', metadata.join(' · '))}`;
236
+ text += `\n\n${jsonHint()}`;
237
+
238
+ return new Text(text, 0, 0);
239
+ }
240
+
241
+ export function renderLinearProjectRelationListCall(
242
+ args: ToolArgs | undefined,
243
+ theme: Theme,
244
+ ): Text {
245
+ return renderLinearToolCall('linear_list_project_relations', args, theme, [
246
+ ['first', 'first'],
247
+ ['orderBy', 'order'],
248
+ ]);
249
+ }
250
+
251
+ export function renderLinearCreateProjectRelationCall(
252
+ args: ToolArgs | undefined,
253
+ theme: Theme,
254
+ ): Text {
255
+ return renderLinearToolCall(
256
+ 'linear_create_project_relation',
257
+ args,
258
+ theme,
259
+ PROJECT_RELATION_MUTATION_FIELDS,
260
+ );
261
+ }
262
+
263
+ export function renderLinearUpdateProjectRelationCall(
264
+ args: ToolArgs | undefined,
265
+ theme: Theme,
266
+ ): Text {
267
+ return renderLinearToolCall(
268
+ 'linear_update_project_relation',
269
+ args,
270
+ theme,
271
+ PROJECT_RELATION_MUTATION_FIELDS,
272
+ );
273
+ }
274
+
275
+ export function renderLinearDeleteProjectRelationCall(
276
+ args: ToolArgs | undefined,
277
+ theme: Theme,
278
+ ): Text {
279
+ return renderLinearToolCall('linear_delete_project_relation', args, theme, [['id', 'id']]);
280
+ }
281
+
282
+ export function renderLinearProjectRelationListResult(
283
+ result: AgentToolResult<any>,
284
+ options: ToolRenderResultOptions,
285
+ theme: Theme,
286
+ context: LinearToolRenderContext,
287
+ ): Text | LinearListResultComponent<ProjectRelationLike> {
288
+ if (options.isPartial) return new Text(theme.fg('warning', 'Loading project relations…'), 0, 0);
289
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
290
+
291
+ const projectRelations = Array.isArray(projectRelationDetails(result).projectRelations)
292
+ ? (projectRelationDetails(result).projectRelations as ProjectRelationLike[])
293
+ : [];
294
+
295
+ return new LinearListResultComponent(projectRelations, theme, {
296
+ noun: 'project relation',
297
+ pluralNoun: 'project relations',
298
+ emptyLabel: 'No project relations found',
299
+ previewLimit: PROJECT_RELATION_LIST_PREVIEW_LIMIT,
300
+ renderItems: renderProjectRelationTable,
301
+ });
302
+ }
303
+
304
+ export function renderLinearProjectRelationResult(actionLabel: string) {
305
+ return (
306
+ result: AgentToolResult<any>,
307
+ options: ToolRenderResultOptions,
308
+ theme: Theme,
309
+ context: LinearToolRenderContext,
310
+ ): Text => {
311
+ if (options.isPartial) return new Text(theme.fg('warning', `${actionLabel}…`), 0, 0);
312
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
313
+
314
+ return renderProjectRelationCard(
315
+ actionLabel,
316
+ projectRelationDetails(result).projectRelation,
317
+ theme,
318
+ );
319
+ };
320
+ }
321
+
322
+ export function renderLinearDeleteProjectRelationResult(
323
+ result: AgentToolResult<any>,
324
+ options: ToolRenderResultOptions,
325
+ theme: Theme,
326
+ context: { args?: unknown },
327
+ ): Text {
328
+ if (options.isPartial) return new Text(theme.fg('warning', 'Deleting project relation…'), 0, 0);
329
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
330
+
331
+ const details = projectRelationDetails(result);
332
+ const args = argsObject(context);
333
+ const id = asString(args.id) ?? 'project relation';
334
+
335
+ if (details.success !== true) {
336
+ return new Text(`\n${theme.fg('warning', 'Delete status unknown')}\n\n${jsonHint()}`, 0, 0);
337
+ }
338
+
339
+ return new Text(
340
+ `\n${theme.fg('success', '✓ Deleted project relation')} ${theme.fg('accent', id)}\n\n${jsonHint()}`,
341
+ 0,
342
+ 0,
343
+ );
344
+ }