@alasano/pi-linear 0.1.1 → 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 (35) 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/renderers/comments.ts +323 -0
  5. package/extensions/renderers/common.ts +305 -0
  6. package/extensions/renderers/documents.ts +326 -0
  7. package/extensions/renderers/initiatives.ts +344 -0
  8. package/extensions/renderers/issue-labels.ts +294 -0
  9. package/extensions/renderers/issue-relations.ts +318 -0
  10. package/extensions/renderers/issue-statuses.ts +199 -0
  11. package/extensions/renderers/issues.ts +373 -0
  12. package/extensions/renderers/milestones.ts +294 -0
  13. package/extensions/renderers/project-labels.ts +279 -0
  14. package/extensions/renderers/project-relations.ts +344 -0
  15. package/extensions/renderers/projects.ts +430 -0
  16. package/extensions/renderers/state.ts +35 -0
  17. package/extensions/renderers/teams.ts +246 -0
  18. package/extensions/renderers/users.ts +242 -0
  19. package/extensions/renderers/workspaces.ts +44 -0
  20. package/extensions/settings.ts +40 -7
  21. package/extensions/tools/comments.ts +17 -0
  22. package/extensions/tools/documents.ts +23 -0
  23. package/extensions/tools/initiatives.ts +24 -0
  24. package/extensions/tools/issue-labels.ts +17 -0
  25. package/extensions/tools/issue-relations.ts +17 -0
  26. package/extensions/tools/issue-statuses.ts +6 -0
  27. package/extensions/tools/issues.ts +29 -0
  28. package/extensions/tools/milestones.ts +18 -0
  29. package/extensions/tools/project-labels.ts +17 -0
  30. package/extensions/tools/project-relations.ts +17 -0
  31. package/extensions/tools/projects.ts +24 -0
  32. package/extensions/tools/teams.ts +10 -0
  33. package/extensions/tools/users.ts +10 -0
  34. package/extensions/tools/workspaces.ts +6 -0
  35. 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
+ asString,
9
+ cleanOneLine,
10
+ expandedJson,
11
+ shouldShowJson,
12
+ jsonHint,
13
+ LinearListResultComponent,
14
+ renderLinearToolCall,
15
+ renderResponsiveTable,
16
+ truncate,
17
+ truncateLine,
18
+ type LinearToolRenderContext,
19
+ type TableColumn,
20
+ type ToolArgs,
21
+ } from './common';
22
+
23
+ type IssueLabelRef = {
24
+ id?: string | null;
25
+ key?: string | null;
26
+ name?: string | null;
27
+ };
28
+
29
+ type IssueLabelLike = {
30
+ id?: string | null;
31
+ name?: string | null;
32
+ description?: string | null;
33
+ color?: string | null;
34
+ isGroup?: boolean | null;
35
+ createdAt?: string | null;
36
+ updatedAt?: string | null;
37
+ retiredAt?: string | null;
38
+ team?: IssueLabelRef | null;
39
+ parent?: IssueLabelRef | null;
40
+ };
41
+
42
+ type IssueLabelResultDetails = {
43
+ label?: IssueLabelLike | null;
44
+ labels?: IssueLabelLike[];
45
+ success?: boolean;
46
+ };
47
+
48
+ const ISSUE_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 issueLabelDetails(result: AgentToolResult<any>): IssueLabelResultDetails {
54
+ return (result.details ?? {}) as IssueLabelResultDetails;
55
+ }
56
+
57
+ function argsObject(context: { args?: unknown }): ToolArgs {
58
+ return context.args && typeof context.args === 'object' && !Array.isArray(context.args)
59
+ ? (context.args as ToolArgs)
60
+ : {};
61
+ }
62
+
63
+ function labelName(label: IssueLabelLike): string {
64
+ return truncate(cleanOneLine(asString(label.name) ?? '(unnamed label)'), NAME_LIMIT);
65
+ }
66
+
67
+ function teamText(label: IssueLabelLike): string | undefined {
68
+ const team = label.team;
69
+ if (!team) return undefined;
70
+ return asString(team.key) ?? asString(team.name) ?? asString(team.id);
71
+ }
72
+
73
+ function parentText(label: IssueLabelLike): string | undefined {
74
+ const parent = label.parent;
75
+ if (!parent) return undefined;
76
+ return asString(parent.name) ?? asString(parent.id);
77
+ }
78
+
79
+ function groupText(label: IssueLabelLike): string {
80
+ return label.isGroup === true ? 'yes' : '—';
81
+ }
82
+
83
+ function colorText(label: IssueLabelLike): string | undefined {
84
+ return asString(label.color);
85
+ }
86
+
87
+ function flagParts(label: IssueLabelLike): string[] {
88
+ return [
89
+ label.isGroup === true ? 'group' : undefined,
90
+ asString(label.retiredAt) ? 'retired' : undefined,
91
+ ].filter((flag): flag is string => !!flag);
92
+ }
93
+
94
+ function descriptionSnippet(label: IssueLabelLike): string | undefined {
95
+ const description = asString(label.description);
96
+ if (!description) return undefined;
97
+ return truncate(cleanOneLine(description), DESCRIPTION_LIMIT);
98
+ }
99
+
100
+ function metadataParts(
101
+ label: IssueLabelLike,
102
+ options: { includeDescription?: boolean } = {},
103
+ ): string[] {
104
+ const team = teamText(label);
105
+ const parent = parentText(label);
106
+ const flags = flagParts(label);
107
+ const color = colorText(label);
108
+ const description = options.includeDescription ? descriptionSnippet(label) : undefined;
109
+
110
+ return [
111
+ team ? `team: ${team}` : undefined,
112
+ parent ? `parent: ${parent}` : undefined,
113
+ flags.length ? flags.join(', ') : undefined,
114
+ color ? `color: ${color}` : undefined,
115
+ description,
116
+ ].filter((part): part is string => !!part);
117
+ }
118
+
119
+ function formatIssueLabelListLine(label: IssueLabelLike, theme: Theme, width: number): string {
120
+ const name = labelName(label);
121
+ const metadata = metadataParts(label, { includeDescription: true });
122
+ const suffix = metadata.length ? theme.fg('dim', ` · ${metadata.join(' · ')}`) : '';
123
+
124
+ return truncateLine(` ${theme.fg('toolOutput', name)}${suffix}`, width);
125
+ }
126
+
127
+ function formatIssueLabelTitle(label: IssueLabelLike, theme: Theme): string {
128
+ const id = asString(label.id);
129
+ const name = theme.fg('toolOutput', labelName(label));
130
+ return id ? `${name} ${theme.fg('dim', `(${truncate(id, 8)})`)}` : name;
131
+ }
132
+
133
+ const ISSUE_LABEL_TABLE_COLUMNS: TableColumn<IssueLabelLike>[] = [
134
+ {
135
+ id: 'team',
136
+ label: 'Team',
137
+ width: 12,
138
+ value: (label) => teamText(label) ?? '—',
139
+ style: (theme, value) => (text) => theme.fg(value === '—' ? 'dim' : 'accent', text),
140
+ },
141
+ {
142
+ id: 'parent',
143
+ label: 'Parent',
144
+ width: 18,
145
+ value: (label) => parentText(label) ?? '—',
146
+ style: (theme, value) => (text) => theme.fg(value === '—' ? 'dim' : 'muted', text),
147
+ },
148
+ {
149
+ id: 'group',
150
+ label: 'Group',
151
+ width: 7,
152
+ value: groupText,
153
+ style: (theme, value) => (text) => theme.fg(value === 'yes' ? 'success' : 'dim', text),
154
+ },
155
+ {
156
+ id: 'color',
157
+ label: 'Color',
158
+ width: 10,
159
+ value: (label) => colorText(label) ?? '—',
160
+ style: (theme, value) => (text) => theme.fg(value === '—' ? 'dim' : 'muted', text),
161
+ },
162
+ ];
163
+
164
+ function renderIssueLabelTable(labels: IssueLabelLike[], theme: Theme, width: number): string[] {
165
+ return renderResponsiveTable(labels, theme, width, {
166
+ columns: ISSUE_LABEL_TABLE_COLUMNS,
167
+ primary: {
168
+ label: 'Name',
169
+ minWidth: TABLE_NAME_MIN_WIDTH,
170
+ value: labelName,
171
+ style: (theme) => (text) => theme.fg('toolOutput', text),
172
+ },
173
+ dropOrder: ['color', 'group', 'parent', 'team'],
174
+ fallback: formatIssueLabelListLine,
175
+ });
176
+ }
177
+
178
+ function renderIssueLabelCard(
179
+ actionLabel: string,
180
+ label: IssueLabelLike | null | undefined,
181
+ theme: Theme,
182
+ ): Text {
183
+ if (!label) {
184
+ return new Text(`\n${theme.fg('dim', 'Issue label not found')}\n\n${jsonHint()}`, 0, 0);
185
+ }
186
+
187
+ const metadata = metadataParts(label);
188
+ const description = descriptionSnippet(label);
189
+
190
+ let text = `\n${theme.fg('success', `✓ ${actionLabel}`)} ${formatIssueLabelTitle(label, 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 renderLinearIssueLabelListCall(args: ToolArgs | undefined, theme: Theme): Text {
199
+ return renderLinearToolCall('linear_list_issue_labels', args, theme, [
200
+ ['teamKey', 'team'],
201
+ ['teamId', 'teamId'],
202
+ ['first', 'first'],
203
+ ['last', 'last'],
204
+ ['orderBy', 'order'],
205
+ ['includeArchived', 'archived'],
206
+ ['filter', 'filter'],
207
+ ]);
208
+ }
209
+
210
+ export function renderLinearCreateIssueLabelCall(args: ToolArgs | undefined, theme: Theme): Text {
211
+ return renderLinearToolCall('linear_create_issue_label', args, theme, [
212
+ ['id', 'id'],
213
+ ['name', 'name'],
214
+ ['teamKey', 'team'],
215
+ ['teamId', 'teamId'],
216
+ ['parentId', 'parentId'],
217
+ ['color', 'color'],
218
+ ['isGroup', 'group'],
219
+ ]);
220
+ }
221
+
222
+ export function renderLinearUpdateIssueLabelCall(args: ToolArgs | undefined, theme: Theme): Text {
223
+ return renderLinearToolCall('linear_update_issue_label', args, theme, [
224
+ ['id', 'id'],
225
+ ['name', 'name'],
226
+ ['parentId', 'parentId'],
227
+ ['color', 'color'],
228
+ ['isGroup', 'group'],
229
+ ]);
230
+ }
231
+
232
+ export function renderLinearDeleteIssueLabelCall(args: ToolArgs | undefined, theme: Theme): Text {
233
+ return renderLinearToolCall('linear_delete_issue_label', args, theme, [['id', 'id']]);
234
+ }
235
+
236
+ export function renderLinearIssueLabelListResult(
237
+ result: AgentToolResult<any>,
238
+ options: ToolRenderResultOptions,
239
+ theme: Theme,
240
+ context: LinearToolRenderContext,
241
+ ): Text | LinearListResultComponent<IssueLabelLike> {
242
+ if (options.isPartial) return new Text(theme.fg('warning', 'Loading issue labels…'), 0, 0);
243
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
244
+
245
+ const labels = Array.isArray(issueLabelDetails(result).labels)
246
+ ? (issueLabelDetails(result).labels as IssueLabelLike[])
247
+ : [];
248
+
249
+ return new LinearListResultComponent(labels, theme, {
250
+ noun: 'label',
251
+ pluralNoun: 'labels',
252
+ emptyLabel: 'No issue labels found',
253
+ previewLimit: ISSUE_LABEL_LIST_PREVIEW_LIMIT,
254
+ renderItems: renderIssueLabelTable,
255
+ });
256
+ }
257
+
258
+ export function renderLinearIssueLabelResult(actionLabel: string) {
259
+ return (
260
+ result: AgentToolResult<any>,
261
+ options: ToolRenderResultOptions,
262
+ theme: Theme,
263
+ context: LinearToolRenderContext,
264
+ ): Text => {
265
+ if (options.isPartial) return new Text(theme.fg('warning', `${actionLabel}…`), 0, 0);
266
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
267
+
268
+ return renderIssueLabelCard(actionLabel, issueLabelDetails(result).label, theme);
269
+ };
270
+ }
271
+
272
+ export function renderLinearIssueLabelDeleteResult(
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 issue label…'), 0, 0);
279
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
280
+
281
+ const details = issueLabelDetails(result);
282
+ const args = argsObject(context);
283
+ const id = asString(args.id) ?? 'issue label';
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 issue label')} ${theme.fg('accent', id)}\n\n${jsonHint()}`,
291
+ 0,
292
+ 0,
293
+ );
294
+ }
@@ -0,0 +1,318 @@
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
+ renderLinearToolCall,
16
+ renderResponsiveTable,
17
+ toolOutputStyle,
18
+ truncate,
19
+ truncateLine,
20
+ type LinearToolRenderContext,
21
+ type TableColumn,
22
+ type ToolArgs,
23
+ } from './common';
24
+
25
+ type NamedIssue = {
26
+ id?: string;
27
+ identifier?: string | null;
28
+ title?: string | null;
29
+ };
30
+
31
+ type IssueRelationLike = {
32
+ id?: string;
33
+ createdAt?: string | null;
34
+ updatedAt?: string | null;
35
+ type?: string | null;
36
+ issue?: NamedIssue | null;
37
+ relatedIssue?: NamedIssue | null;
38
+ };
39
+
40
+ type IssueRelationResultDetails = {
41
+ issueRelation?: IssueRelationLike | null;
42
+ issueRelations?: IssueRelationLike[];
43
+ success?: boolean;
44
+ };
45
+
46
+ const ISSUE_RELATION_LIST_PREVIEW_LIMIT = 20;
47
+ const ISSUE_TITLE_LIMIT = 54;
48
+ const TABLE_RELATION_MIN_WIDTH = 24;
49
+
50
+ function issueRelationDetails(result: AgentToolResult<any>): IssueRelationResultDetails {
51
+ return (result.details ?? {}) as IssueRelationResultDetails;
52
+ }
53
+
54
+ function argsObject(context: { args?: unknown }): ToolArgs {
55
+ return context.args && typeof context.args === 'object' && !Array.isArray(context.args)
56
+ ? (context.args as ToolArgs)
57
+ : {};
58
+ }
59
+
60
+ function dateText(value: unknown): string | undefined {
61
+ const date = asString(value);
62
+ if (!date) return undefined;
63
+
64
+ const timeSeparator = date.indexOf('T');
65
+ return timeSeparator >= 0 ? date.slice(0, timeSeparator) : date;
66
+ }
67
+
68
+ function issueRef(issue: NamedIssue | null | undefined): string | undefined {
69
+ return asString(issue?.identifier) ?? asString(issue?.id);
70
+ }
71
+
72
+ function issueTitle(issue: NamedIssue | null | undefined): string | undefined {
73
+ const title = asString(issue?.title);
74
+ return title ? truncate(cleanOneLine(title), ISSUE_TITLE_LIMIT) : undefined;
75
+ }
76
+
77
+ function relationType(relation: IssueRelationLike): string {
78
+ return asString(relation.type) ?? 'related';
79
+ }
80
+
81
+ function relationVerb(relation: IssueRelationLike): string {
82
+ const type = relationType(relation).toLowerCase();
83
+ if (type === 'blocks') return 'blocks';
84
+ if (type === 'duplicate') return 'duplicates';
85
+ if (type === 'related') return 'related to';
86
+ if (type === 'similar') return 'similar to';
87
+ return type.replace(/[_-]+/g, ' ');
88
+ }
89
+
90
+ function relationId(relation: IssueRelationLike): string | undefined {
91
+ return asString(relation.id);
92
+ }
93
+
94
+ function relationSummary(relation: IssueRelationLike): string {
95
+ const issue = issueRef(relation.issue);
96
+ const relatedIssue = issueRef(relation.relatedIssue);
97
+ if (issue && relatedIssue) return `${issue} ${relationVerb(relation)} ${relatedIssue}`;
98
+
99
+ return (
100
+ relationId(relation) ?? [issue, relationVerb(relation), relatedIssue].filter(Boolean).join(' ')
101
+ );
102
+ }
103
+
104
+ function formatIssueWithTitle(issue: NamedIssue | null | undefined, theme: Theme): string {
105
+ const ref = issueRef(issue);
106
+ const title = issueTitle(issue);
107
+
108
+ if (ref && title) return `${theme.fg('accent', ref)} ${theme.fg('toolOutput', title)}`;
109
+ if (ref) return theme.fg('accent', ref);
110
+ if (title) return theme.fg('toolOutput', title);
111
+ return theme.fg('dim', 'unknown issue');
112
+ }
113
+
114
+ function formatRelationInline(relation: IssueRelationLike, theme: Theme): string {
115
+ const issue = issueRef(relation.issue);
116
+ const relatedIssue = issueRef(relation.relatedIssue);
117
+ if (issue && relatedIssue) {
118
+ return `${theme.fg('accent', issue)} ${theme.fg('toolOutput', relationVerb(relation))} ${theme.fg(
119
+ 'accent',
120
+ relatedIssue,
121
+ )}`;
122
+ }
123
+
124
+ return theme.fg('toolOutput', relationSummary(relation));
125
+ }
126
+
127
+ function formatRelationCardSummary(relation: IssueRelationLike, theme: Theme): string {
128
+ const issue = issueRef(relation.issue) ?? issueTitle(relation.issue);
129
+ const relatedIssue = issueRef(relation.relatedIssue) ?? issueTitle(relation.relatedIssue);
130
+ if (!issue && !relatedIssue) return theme.fg('accent', relationId(relation) ?? 'issue relation');
131
+
132
+ return `${formatIssueWithTitle(relation.issue, theme)} ${theme.fg(
133
+ 'toolOutput',
134
+ relationVerb(relation),
135
+ )} ${formatIssueWithTitle(relation.relatedIssue, theme)}`;
136
+ }
137
+
138
+ function listMetadataParts(relation: IssueRelationLike): string[] {
139
+ const updated = dateText(relation.updatedAt);
140
+ const id = relationId(relation);
141
+
142
+ return [updated ? `updated: ${updated}` : undefined, id ? `id: ${id}` : undefined].filter(
143
+ (part): part is string => !!part,
144
+ );
145
+ }
146
+
147
+ function cardMetadataParts(relation: IssueRelationLike): string[] {
148
+ const updated = dateText(relation.updatedAt);
149
+ const created = dateText(relation.createdAt);
150
+ const id = relationId(relation);
151
+
152
+ return [
153
+ updated ? `updated: ${updated}` : undefined,
154
+ created ? `created: ${created}` : undefined,
155
+ id ? `id: ${id}` : undefined,
156
+ ].filter((part): part is string => !!part);
157
+ }
158
+
159
+ function formatIssueRelationListLine(
160
+ relation: IssueRelationLike,
161
+ theme: Theme,
162
+ width: number,
163
+ ): string {
164
+ const metadata = listMetadataParts(relation);
165
+ const suffix = metadata.length ? theme.fg('dim', ` · ${metadata.join(' · ')}`) : '';
166
+
167
+ return truncateLine(` ${formatRelationInline(relation, theme)}${suffix}`, width);
168
+ }
169
+
170
+ const ISSUE_RELATION_TABLE_COLUMNS: TableColumn<IssueRelationLike>[] = [
171
+ {
172
+ id: 'updated',
173
+ label: 'Updated',
174
+ width: 10,
175
+ value: (relation) => dateText(relation.updatedAt) ?? '—',
176
+ style: (theme) => dimStyle(theme),
177
+ },
178
+ ];
179
+
180
+ function renderIssueRelationTable(
181
+ issueRelations: IssueRelationLike[],
182
+ theme: Theme,
183
+ width: number,
184
+ ): string[] {
185
+ return renderResponsiveTable(issueRelations, theme, width, {
186
+ columns: ISSUE_RELATION_TABLE_COLUMNS,
187
+ primary: {
188
+ label: 'Relation',
189
+ minWidth: TABLE_RELATION_MIN_WIDTH,
190
+ value: relationSummary,
191
+ style: (theme) => toolOutputStyle(theme),
192
+ },
193
+ dropOrder: ['updated'],
194
+ fallback: formatIssueRelationListLine,
195
+ });
196
+ }
197
+
198
+ function renderIssueRelationCard(
199
+ actionLabel: string,
200
+ relation: IssueRelationLike | null | undefined,
201
+ theme: Theme,
202
+ ): Text {
203
+ if (!relation) {
204
+ return new Text(`\n${theme.fg('dim', 'Issue relation not found')}\n\n${jsonHint()}`, 0, 0);
205
+ }
206
+
207
+ const metadata = cardMetadataParts(relation);
208
+ let text = `\n${theme.fg('success', `✓ ${actionLabel}`)} ${formatRelationCardSummary(
209
+ relation,
210
+ theme,
211
+ )}`;
212
+ if (metadata.length) text += `\n ${theme.fg('dim', metadata.join(' · '))}`;
213
+ text += `\n\n${jsonHint()}`;
214
+
215
+ return new Text(text, 0, 0);
216
+ }
217
+
218
+ export function renderLinearIssueRelationListCall(args: ToolArgs | undefined, theme: Theme): Text {
219
+ return renderLinearToolCall('linear_list_issue_relations', args, theme, [
220
+ ['first', 'first'],
221
+ ['last', 'last'],
222
+ ['orderBy', 'order'],
223
+ ['includeArchived', 'archived'],
224
+ ]);
225
+ }
226
+
227
+ export function renderLinearCreateIssueRelationCall(
228
+ args: ToolArgs | undefined,
229
+ theme: Theme,
230
+ ): Text {
231
+ return renderLinearToolCall('linear_create_issue_relation', args, theme, [
232
+ ['issueId', 'issueId'],
233
+ ['relatedIssueId', 'relatedIssueId'],
234
+ ['type', 'type'],
235
+ ]);
236
+ }
237
+
238
+ export function renderLinearUpdateIssueRelationCall(
239
+ args: ToolArgs | undefined,
240
+ theme: Theme,
241
+ ): Text {
242
+ return renderLinearToolCall('linear_update_issue_relation', args, theme, [
243
+ ['id', 'id'],
244
+ ['issueId', 'issueId'],
245
+ ['relatedIssueId', 'relatedIssueId'],
246
+ ['type', 'type'],
247
+ ]);
248
+ }
249
+
250
+ export function renderLinearDeleteIssueRelationCall(
251
+ args: ToolArgs | undefined,
252
+ theme: Theme,
253
+ ): Text {
254
+ return renderLinearToolCall('linear_delete_issue_relation', args, theme, [['id', 'id']]);
255
+ }
256
+
257
+ export function renderLinearIssueRelationListResult(
258
+ result: AgentToolResult<any>,
259
+ options: ToolRenderResultOptions,
260
+ theme: Theme,
261
+ context: LinearToolRenderContext,
262
+ ): Text | LinearListResultComponent<IssueRelationLike> {
263
+ if (options.isPartial) return new Text(theme.fg('warning', 'Loading issue relations…'), 0, 0);
264
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
265
+
266
+ const issueRelations = Array.isArray(issueRelationDetails(result).issueRelations)
267
+ ? (issueRelationDetails(result).issueRelations as IssueRelationLike[])
268
+ : [];
269
+
270
+ return new LinearListResultComponent(issueRelations, theme, {
271
+ noun: 'issue relation',
272
+ emptyLabel: 'No issue relations found',
273
+ previewLimit: ISSUE_RELATION_LIST_PREVIEW_LIMIT,
274
+ renderItems: renderIssueRelationTable,
275
+ });
276
+ }
277
+
278
+ export function renderLinearIssueRelationResult(actionLabel: string) {
279
+ return (
280
+ result: AgentToolResult<any>,
281
+ options: ToolRenderResultOptions,
282
+ theme: Theme,
283
+ context: LinearToolRenderContext,
284
+ ): Text => {
285
+ if (options.isPartial) return new Text(theme.fg('warning', `${actionLabel}…`), 0, 0);
286
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
287
+
288
+ return renderIssueRelationCard(actionLabel, issueRelationDetails(result).issueRelation, theme);
289
+ };
290
+ }
291
+
292
+ export function renderLinearDeleteIssueRelationResult(
293
+ result: AgentToolResult<any>,
294
+ options: ToolRenderResultOptions,
295
+ theme: Theme,
296
+ context: { args?: unknown },
297
+ ): Text {
298
+ if (options.isPartial) return new Text(theme.fg('warning', 'Deleting issue relation…'), 0, 0);
299
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
300
+
301
+ const details = issueRelationDetails(result);
302
+ const args = argsObject(context);
303
+ const id = asString(args.id) ?? 'issue relation';
304
+
305
+ if (details.success !== true) {
306
+ return new Text(
307
+ `\n${theme.fg('warning', 'Deleted issue relation status unknown')}\n\n${jsonHint()}`,
308
+ 0,
309
+ 0,
310
+ );
311
+ }
312
+
313
+ return new Text(
314
+ `\n${theme.fg('success', '✓ Deleted issue relation')} ${theme.fg('accent', id)}\n\n${jsonHint()}`,
315
+ 0,
316
+ 0,
317
+ );
318
+ }