@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,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 InitiativeLike = {
27
+ id?: string | null;
28
+ name?: string | null;
29
+ description?: string | null;
30
+ content?: string | null;
31
+ status?: string | null;
32
+ targetDate?: string | null;
33
+ health?: string | null;
34
+ completedAt?: string | null;
35
+ startedAt?: string | null;
36
+ archivedAt?: string | null;
37
+ trashed?: boolean | null;
38
+ url?: string | null;
39
+ owner?: { id?: string; name?: string | null; email?: string | null } | null;
40
+ };
41
+
42
+ type InitiativeResultDetails = {
43
+ initiative?: InitiativeLike | null;
44
+ initiatives?: InitiativeLike[];
45
+ success?: boolean;
46
+ };
47
+
48
+ const INITIATIVE_LIST_PREVIEW_LIMIT = 20;
49
+ const NAME_LIMIT = 90;
50
+ const SNIPPET_LIMIT = 180;
51
+ const TABLE_NAME_MIN_WIDTH = 24;
52
+
53
+ function initiativeDetails(result: AgentToolResult<any>): InitiativeResultDetails {
54
+ return (result.details ?? {}) as InitiativeResultDetails;
55
+ }
56
+
57
+ function humanizeEnum(value: string): string {
58
+ const words = cleanOneLine(value)
59
+ .replace(/[_-]+/g, ' ')
60
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
61
+ .toLowerCase();
62
+ return words.replace(/\b\w/g, (character) => character.toUpperCase());
63
+ }
64
+
65
+ function initiativeName(initiative: InitiativeLike): string {
66
+ return truncate(cleanOneLine(asString(initiative.name) ?? '(untitled)'), NAME_LIMIT);
67
+ }
68
+
69
+ function ownerName(initiative: InitiativeLike): string | undefined {
70
+ return asString(initiative.owner?.name) ?? asString(initiative.owner?.email);
71
+ }
72
+
73
+ function statusText(initiative: InitiativeLike): string | undefined {
74
+ const status = asString(initiative.status);
75
+ return status ? humanizeEnum(status) : undefined;
76
+ }
77
+
78
+ function healthText(initiative: InitiativeLike): string | undefined {
79
+ const health = asString(initiative.health);
80
+ return health ? humanizeEnum(health) : undefined;
81
+ }
82
+
83
+ function metadataParts(initiative: InitiativeLike, options: { includeFlags?: boolean } = {}) {
84
+ const owner = ownerName(initiative);
85
+ const targetDate = asString(initiative.targetDate);
86
+
87
+ const parts = [
88
+ statusText(initiative),
89
+ healthText(initiative),
90
+ owner ? `@${owner}` : undefined,
91
+ targetDate ? `target ${targetDate}` : undefined,
92
+ ];
93
+
94
+ if (options.includeFlags) {
95
+ const startedAt = asString(initiative.startedAt);
96
+ const completedAt = asString(initiative.completedAt);
97
+ const archivedAt = asString(initiative.archivedAt);
98
+ parts.push(
99
+ startedAt ? `started ${startedAt}` : undefined,
100
+ completedAt ? `completed ${completedAt}` : undefined,
101
+ archivedAt ? `archived ${archivedAt}` : undefined,
102
+ initiative.trashed === true ? 'trashed' : undefined,
103
+ );
104
+ }
105
+
106
+ return parts.filter((part): part is string => !!part);
107
+ }
108
+
109
+ function descriptionSnippet(initiative: InitiativeLike): string | undefined {
110
+ const text = asString(initiative.description) ?? asString(initiative.content);
111
+ if (!text) return undefined;
112
+ return truncate(cleanOneLine(text), SNIPPET_LIMIT);
113
+ }
114
+
115
+ function formatInitiativeListLine(initiative: InitiativeLike, theme: Theme, width: number): string {
116
+ const name = initiativeName(initiative);
117
+ const metadata = metadataParts(initiative);
118
+ const suffix = metadata.length ? theme.fg('dim', ` · ${metadata.join(' · ')}`) : '';
119
+
120
+ return truncateLine(` ${theme.fg('toolOutput', name)}${suffix}`, width);
121
+ }
122
+
123
+ function statusStyle(theme: Theme, value: string): (text: string) => string {
124
+ const normalized = value.toLowerCase();
125
+ if (normalized === 'completed' || normalized === 'done')
126
+ return (text) => theme.fg('success', text);
127
+ if (normalized === 'canceled' || normalized === 'cancelled')
128
+ return (text) => theme.fg('error', text);
129
+ if (normalized === 'planned' || value === '—') return dimStyle(theme);
130
+ return mutedStyle(theme);
131
+ }
132
+
133
+ function healthStyle(theme: Theme, value: string): (text: string) => string {
134
+ const normalized = value.toLowerCase();
135
+ if (normalized.includes('off')) return (text) => theme.fg('error', text);
136
+ if (normalized.includes('risk')) return (text) => theme.fg('warning', text);
137
+ if (normalized.includes('track')) return (text) => theme.fg('success', text);
138
+ if (value === '—') return dimStyle(theme);
139
+ return mutedStyle(theme);
140
+ }
141
+
142
+ const INITIATIVE_TABLE_COLUMNS: TableColumn<InitiativeLike>[] = [
143
+ {
144
+ id: 'status',
145
+ label: 'Status',
146
+ width: 14,
147
+ value: (initiative) => statusText(initiative) ?? '—',
148
+ style: statusStyle,
149
+ },
150
+ {
151
+ id: 'health',
152
+ label: 'Health',
153
+ width: 12,
154
+ value: (initiative) => healthText(initiative) ?? '—',
155
+ style: healthStyle,
156
+ },
157
+ {
158
+ id: 'owner',
159
+ label: 'Owner',
160
+ width: 18,
161
+ value: (initiative) => ownerName(initiative) ?? '—',
162
+ style: (theme) => mutedStyle(theme),
163
+ },
164
+ {
165
+ id: 'target',
166
+ label: 'Target',
167
+ width: 12,
168
+ value: (initiative) => asString(initiative.targetDate) ?? '—',
169
+ style: (theme) => dimStyle(theme),
170
+ },
171
+ ];
172
+
173
+ function renderInitiativeTable(
174
+ initiatives: InitiativeLike[],
175
+ theme: Theme,
176
+ width: number,
177
+ ): string[] {
178
+ return renderResponsiveTable(initiatives, theme, width, {
179
+ columns: INITIATIVE_TABLE_COLUMNS,
180
+ primary: {
181
+ label: 'Name',
182
+ minWidth: TABLE_NAME_MIN_WIDTH,
183
+ value: initiativeName,
184
+ style: (theme) => toolOutputStyle(theme),
185
+ },
186
+ dropOrder: ['target', 'owner', 'health', 'status'],
187
+ fallback: formatInitiativeListLine,
188
+ });
189
+ }
190
+
191
+ function renderInitiativeCard(
192
+ result: AgentToolResult<any>,
193
+ options: ToolRenderResultOptions,
194
+ theme: Theme,
195
+ context: LinearToolRenderContext,
196
+ actionLabel: string,
197
+ ): Text {
198
+ if (options.isPartial) return new Text(theme.fg('warning', `${actionLabel}…`), 0, 0);
199
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
200
+
201
+ const initiative = initiativeDetails(result).initiative;
202
+ if (!initiative) {
203
+ return new Text(`\n${theme.fg('dim', 'Initiative not found')}\n\n${jsonHint()}`, 0, 0);
204
+ }
205
+
206
+ const name = initiativeName(initiative);
207
+ const metadata = metadataParts(initiative, { includeFlags: true });
208
+ const snippet = descriptionSnippet(initiative);
209
+ const url = asString(initiative.url);
210
+
211
+ let text = `\n${theme.fg('success', `✓ ${actionLabel}`)} ${theme.fg('toolOutput', name)}`;
212
+ if (metadata.length) text += `\n ${theme.fg('dim', metadata.join(' · '))}`;
213
+ if (snippet) text += `\n ${theme.fg('muted', snippet)}`;
214
+ if (url) text += `\n ${theme.fg('dim', url)}`;
215
+ text += `\n\n${jsonHint()}`;
216
+
217
+ return new Text(text, 0, 0);
218
+ }
219
+
220
+ export function renderLinearInitiativeListCall(args: ToolArgs | undefined, theme: Theme): Text {
221
+ return renderLinearToolCall('linear_list_initiatives', args, theme, [
222
+ ['first', 'first'],
223
+ ['last', 'last'],
224
+ ['orderBy', 'order'],
225
+ ['includeArchived', 'archived'],
226
+ ['filter', 'filter'],
227
+ ['sort', 'sort'],
228
+ ]);
229
+ }
230
+
231
+ export function renderLinearGetInitiativeCall(args: ToolArgs | undefined, theme: Theme): Text {
232
+ return renderLinearToolCall('linear_get_initiative', args, theme, [
233
+ ['initiativeId', 'initiativeId'],
234
+ ]);
235
+ }
236
+
237
+ export function renderLinearSaveInitiativeCall(args: ToolArgs | undefined, theme: Theme): Text {
238
+ return renderLinearToolCall('linear_save_initiative', args, theme, [
239
+ ['initiativeId', 'initiativeId'],
240
+ ['name', 'name'],
241
+ ['status', 'status'],
242
+ ['ownerId', 'ownerId'],
243
+ ['targetDate', 'target'],
244
+ ]);
245
+ }
246
+
247
+ export function renderLinearDeleteInitiativeCall(args: ToolArgs | undefined, theme: Theme): Text {
248
+ return renderLinearToolCall('linear_delete_initiative', args, theme, [
249
+ ['initiativeId', 'initiativeId'],
250
+ ]);
251
+ }
252
+
253
+ export function renderLinearArchiveInitiativeCall(args: ToolArgs | undefined, theme: Theme): Text {
254
+ return renderLinearToolCall('linear_archive_initiative', args, theme, [
255
+ ['initiativeId', 'initiativeId'],
256
+ ]);
257
+ }
258
+
259
+ export function renderLinearUnarchiveInitiativeCall(
260
+ args: ToolArgs | undefined,
261
+ theme: Theme,
262
+ ): Text {
263
+ return renderLinearToolCall('linear_unarchive_initiative', args, theme, [
264
+ ['initiativeId', 'initiativeId'],
265
+ ]);
266
+ }
267
+
268
+ export function renderLinearInitiativeListResult(
269
+ result: AgentToolResult<any>,
270
+ options: ToolRenderResultOptions,
271
+ theme: Theme,
272
+ context: LinearToolRenderContext,
273
+ ): Text | LinearListResultComponent<InitiativeLike> {
274
+ if (options.isPartial) return new Text(theme.fg('warning', 'Loading initiatives…'), 0, 0);
275
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
276
+
277
+ const initiatives = Array.isArray(initiativeDetails(result).initiatives)
278
+ ? (initiativeDetails(result).initiatives as InitiativeLike[])
279
+ : [];
280
+
281
+ return new LinearListResultComponent(initiatives, theme, {
282
+ noun: 'initiative',
283
+ emptyLabel: 'No initiatives found',
284
+ previewLimit: INITIATIVE_LIST_PREVIEW_LIMIT,
285
+ renderItems: renderInitiativeTable,
286
+ });
287
+ }
288
+
289
+ export function renderLinearInitiativeResult(actionLabel: string) {
290
+ return (
291
+ result: AgentToolResult<any>,
292
+ options: ToolRenderResultOptions,
293
+ theme: Theme,
294
+ context: LinearToolRenderContext,
295
+ ): Text => renderInitiativeCard(result, options, theme, context, actionLabel);
296
+ }
297
+
298
+ export function renderLinearSaveInitiativeResult(
299
+ result: AgentToolResult<any>,
300
+ options: ToolRenderResultOptions,
301
+ theme: Theme,
302
+ context: { args?: unknown },
303
+ ): Text {
304
+ const args = (context.args ?? {}) as { initiativeId?: unknown };
305
+ const actionLabel = asString(args.initiativeId) ? 'Updated initiative' : 'Created initiative';
306
+ return renderInitiativeCard(result, options, theme, context, actionLabel);
307
+ }
308
+
309
+ export function renderLinearInitiativeSuccessResult(defaultActionLabel: string) {
310
+ return (
311
+ result: AgentToolResult<any>,
312
+ options: ToolRenderResultOptions,
313
+ theme: Theme,
314
+ context: { args?: unknown },
315
+ ): Text => {
316
+ if (options.isPartial)
317
+ return new Text(theme.fg('warning', `${defaultActionLabel} initiative…`), 0, 0);
318
+ if (shouldShowJson(options, context)) return expandedJson(result, theme);
319
+
320
+ const details = initiativeDetails(result);
321
+ const args = (context.args ?? {}) as { initiativeId?: unknown };
322
+ const initiativeId = asString(args.initiativeId) ?? 'initiative';
323
+
324
+ if (details.success !== true) {
325
+ return new Text(
326
+ `\n${theme.fg('warning', `${defaultActionLabel} status unknown`)} ${theme.fg(
327
+ 'accent',
328
+ initiativeId,
329
+ )}\n\n${jsonHint()}`,
330
+ 0,
331
+ 0,
332
+ );
333
+ }
334
+
335
+ return new Text(
336
+ `\n${theme.fg('success', `✓ ${defaultActionLabel}`)} ${theme.fg(
337
+ 'accent',
338
+ initiativeId,
339
+ )}\n\n${jsonHint()}`,
340
+ 0,
341
+ 0,
342
+ );
343
+ };
344
+ }
@@ -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
+ }