@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.
- package/README.md +14 -2
- package/assets/linear_list_issues.png +0 -0
- package/assets/screenshot.png +0 -0
- package/extensions/index.ts +1 -1
- package/extensions/renderers/comments.ts +323 -0
- package/extensions/renderers/common.ts +305 -0
- package/extensions/renderers/documents.ts +326 -0
- package/extensions/renderers/initiatives.ts +344 -0
- package/extensions/renderers/issue-labels.ts +294 -0
- package/extensions/renderers/issue-relations.ts +318 -0
- package/extensions/renderers/issue-statuses.ts +199 -0
- package/extensions/renderers/issues.ts +373 -0
- package/extensions/renderers/milestones.ts +294 -0
- package/extensions/renderers/project-labels.ts +279 -0
- package/extensions/renderers/project-relations.ts +344 -0
- package/extensions/renderers/projects.ts +430 -0
- package/extensions/renderers/state.ts +35 -0
- package/extensions/renderers/teams.ts +246 -0
- package/extensions/renderers/users.ts +242 -0
- package/extensions/renderers/workspaces.ts +44 -0
- package/extensions/settings.ts +53 -23
- package/extensions/tools/comments.ts +17 -0
- package/extensions/tools/documents.ts +23 -0
- package/extensions/tools/initiatives.ts +24 -0
- package/extensions/tools/issue-labels.ts +17 -0
- package/extensions/tools/issue-relations.ts +29 -5
- package/extensions/tools/issue-statuses.ts +6 -0
- package/extensions/tools/issues.ts +29 -0
- package/extensions/tools/milestones.ts +18 -0
- package/extensions/tools/project-labels.ts +17 -0
- package/extensions/tools/project-relations.ts +17 -0
- package/extensions/tools/projects.ts +25 -1
- package/extensions/tools/teams.ts +11 -3
- package/extensions/tools/users.ts +10 -0
- package/extensions/tools/workspaces.ts +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,326 @@
|
|
|
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 NamedRef = {
|
|
28
|
+
id?: string;
|
|
29
|
+
identifier?: string | null;
|
|
30
|
+
key?: string | null;
|
|
31
|
+
name?: string | null;
|
|
32
|
+
title?: string | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type DocumentLike = {
|
|
36
|
+
id?: string;
|
|
37
|
+
title?: string | null;
|
|
38
|
+
content?: string | null;
|
|
39
|
+
summary?: string | null;
|
|
40
|
+
hiddenAt?: string | null;
|
|
41
|
+
archivedAt?: string | null;
|
|
42
|
+
trashed?: boolean | null;
|
|
43
|
+
updatedAt?: string | null;
|
|
44
|
+
url?: string | null;
|
|
45
|
+
team?: NamedRef | null;
|
|
46
|
+
project?: NamedRef | null;
|
|
47
|
+
issue?: NamedRef | null;
|
|
48
|
+
initiative?: NamedRef | null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type DocumentResultDetails = {
|
|
52
|
+
document?: DocumentLike | null;
|
|
53
|
+
documents?: DocumentLike[];
|
|
54
|
+
success?: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const DOCUMENT_LIST_PREVIEW_LIMIT = 20;
|
|
58
|
+
const TITLE_LIMIT = 90;
|
|
59
|
+
const SNIPPET_LIMIT = 180;
|
|
60
|
+
const TABLE_TITLE_MIN_WIDTH = 24;
|
|
61
|
+
|
|
62
|
+
function documentDetails(result: AgentToolResult<any>): DocumentResultDetails {
|
|
63
|
+
return (result.details ?? {}) as DocumentResultDetails;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function documentTitle(document: DocumentLike): string {
|
|
67
|
+
return truncate(cleanOneLine(asString(document.title) ?? '(untitled)'), TITLE_LIMIT);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function contextText(document: DocumentLike): string | undefined {
|
|
71
|
+
const issue = document.issue;
|
|
72
|
+
if (issue) {
|
|
73
|
+
return asString(issue.identifier) ?? asString(issue.title) ?? asString(issue.id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const project = document.project;
|
|
77
|
+
if (project) return asString(project.name) ?? asString(project.id);
|
|
78
|
+
|
|
79
|
+
const initiative = document.initiative;
|
|
80
|
+
if (initiative) return asString(initiative.name) ?? asString(initiative.id);
|
|
81
|
+
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function teamText(document: DocumentLike): string | undefined {
|
|
86
|
+
const team = document.team;
|
|
87
|
+
if (!team) return undefined;
|
|
88
|
+
return asString(team.key) ?? asString(team.name) ?? asString(team.id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function dateText(value: unknown): string | undefined {
|
|
92
|
+
const date = asString(value);
|
|
93
|
+
if (!date) return undefined;
|
|
94
|
+
return date.includes('T') ? date.split('T')[0] : date;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function documentFlags(document: DocumentLike): string[] {
|
|
98
|
+
return [
|
|
99
|
+
asString(document.hiddenAt) ? 'hidden' : undefined,
|
|
100
|
+
asString(document.archivedAt) ? 'archived' : undefined,
|
|
101
|
+
document.trashed === true ? 'trashed' : undefined,
|
|
102
|
+
].filter((flag): flag is string => !!flag);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function flagsText(document: DocumentLike): string {
|
|
106
|
+
return documentFlags(document).join(', ') || '—';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function documentSnippet(document: DocumentLike): string | undefined {
|
|
110
|
+
const summary = asString(document.summary);
|
|
111
|
+
if (summary) return truncate(cleanOneLine(summary), SNIPPET_LIMIT);
|
|
112
|
+
|
|
113
|
+
const content = asString(document.content);
|
|
114
|
+
if (content) return truncate(cleanOneLine(content), SNIPPET_LIMIT);
|
|
115
|
+
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatDocumentTitle(document: DocumentLike, theme: Theme): string {
|
|
120
|
+
return theme.fg('toolOutput', documentTitle(document));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function metadataParts(document: DocumentLike): string[] {
|
|
124
|
+
const context = contextText(document);
|
|
125
|
+
const team = teamText(document);
|
|
126
|
+
const updated = dateText(document.updatedAt);
|
|
127
|
+
const flags = flagsText(document);
|
|
128
|
+
|
|
129
|
+
return [
|
|
130
|
+
context ? `context: ${context}` : undefined,
|
|
131
|
+
team ? `team: ${team}` : undefined,
|
|
132
|
+
updated ? `updated: ${updated}` : undefined,
|
|
133
|
+
flags !== '—' ? flags : undefined,
|
|
134
|
+
].filter((part): part is string => !!part);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatDocumentListLine(document: DocumentLike, theme: Theme, width: number): string {
|
|
138
|
+
const title = documentTitle(document);
|
|
139
|
+
const metadata = metadataParts(document);
|
|
140
|
+
const suffix = metadata.length ? theme.fg('dim', ` · ${metadata.join(' · ')}`) : '';
|
|
141
|
+
|
|
142
|
+
return truncateLine(` ${theme.fg('toolOutput', title)}${suffix}`, width);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function flagsStyle(theme: Theme, value: string): (text: string) => string {
|
|
146
|
+
if (value === '—') return dimStyle(theme);
|
|
147
|
+
return (text) => theme.fg('warning', text);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const DOCUMENT_TABLE_COLUMNS: TableColumn<DocumentLike>[] = [
|
|
151
|
+
{
|
|
152
|
+
id: 'context',
|
|
153
|
+
label: 'Context',
|
|
154
|
+
width: 18,
|
|
155
|
+
value: (document) => contextText(document) ?? '—',
|
|
156
|
+
style: (theme) => accentStyle(theme),
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'team',
|
|
160
|
+
label: 'Team',
|
|
161
|
+
width: 12,
|
|
162
|
+
value: (document) => teamText(document) ?? '—',
|
|
163
|
+
style: (theme) => mutedStyle(theme),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'updated',
|
|
167
|
+
label: 'Updated',
|
|
168
|
+
width: 10,
|
|
169
|
+
value: (document) => dateText(document.updatedAt) ?? '—',
|
|
170
|
+
style: (theme) => dimStyle(theme),
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'flags',
|
|
174
|
+
label: 'Flags',
|
|
175
|
+
width: 18,
|
|
176
|
+
value: flagsText,
|
|
177
|
+
style: flagsStyle,
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
function renderDocumentTable(documents: DocumentLike[], theme: Theme, width: number): string[] {
|
|
182
|
+
return renderResponsiveTable(documents, theme, width, {
|
|
183
|
+
columns: DOCUMENT_TABLE_COLUMNS,
|
|
184
|
+
primary: {
|
|
185
|
+
label: 'Title',
|
|
186
|
+
minWidth: TABLE_TITLE_MIN_WIDTH,
|
|
187
|
+
value: documentTitle,
|
|
188
|
+
style: (theme) => toolOutputStyle(theme),
|
|
189
|
+
},
|
|
190
|
+
dropOrder: ['flags', 'updated', 'team', 'context'],
|
|
191
|
+
fallback: formatDocumentListLine,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function renderLinearDocumentListCall(args: ToolArgs | undefined, theme: Theme): Text {
|
|
196
|
+
return renderLinearToolCall('linear_list_documents', args, theme, [
|
|
197
|
+
['first', 'first'],
|
|
198
|
+
['last', 'last'],
|
|
199
|
+
['orderBy', 'order'],
|
|
200
|
+
['includeArchived', 'archived'],
|
|
201
|
+
['filter', 'filter'],
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function renderLinearGetDocumentCall(args: ToolArgs | undefined, theme: Theme): Text {
|
|
206
|
+
return renderLinearToolCall('linear_get_document', args, theme, [['documentId', 'documentId']]);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function renderLinearCreateDocumentCall(args: ToolArgs | undefined, theme: Theme): Text {
|
|
210
|
+
return renderLinearToolCall('linear_create_document', args, theme, [
|
|
211
|
+
['title', 'title'],
|
|
212
|
+
['teamKey', 'team'],
|
|
213
|
+
['teamId', 'teamId'],
|
|
214
|
+
['projectId', 'projectId'],
|
|
215
|
+
['issueId', 'issueId'],
|
|
216
|
+
['initiativeId', 'initiativeId'],
|
|
217
|
+
]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function renderLinearUpdateDocumentCall(args: ToolArgs | undefined, theme: Theme): Text {
|
|
221
|
+
return renderLinearToolCall('linear_update_document', args, theme, [
|
|
222
|
+
['documentId', 'documentId'],
|
|
223
|
+
['title', 'title'],
|
|
224
|
+
['teamKey', 'team'],
|
|
225
|
+
['teamId', 'teamId'],
|
|
226
|
+
['projectId', 'projectId'],
|
|
227
|
+
['issueId', 'issueId'],
|
|
228
|
+
['initiativeId', 'initiativeId'],
|
|
229
|
+
]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function renderLinearDeleteDocumentCall(args: ToolArgs | undefined, theme: Theme): Text {
|
|
233
|
+
return renderLinearToolCall('linear_delete_document', args, theme, [
|
|
234
|
+
['documentId', 'documentId'],
|
|
235
|
+
]);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function renderLinearUnarchiveDocumentCall(args: ToolArgs | undefined, theme: Theme): Text {
|
|
239
|
+
return renderLinearToolCall('linear_unarchive_document', args, theme, [
|
|
240
|
+
['documentId', 'documentId'],
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function renderLinearDocumentListResult(
|
|
245
|
+
result: AgentToolResult<any>,
|
|
246
|
+
options: ToolRenderResultOptions,
|
|
247
|
+
theme: Theme,
|
|
248
|
+
context: LinearToolRenderContext,
|
|
249
|
+
): Text | LinearListResultComponent<DocumentLike> {
|
|
250
|
+
if (options.isPartial) return new Text(theme.fg('warning', 'Loading documents…'), 0, 0);
|
|
251
|
+
if (shouldShowJson(options, context)) return expandedJson(result, theme);
|
|
252
|
+
|
|
253
|
+
const documents = Array.isArray(documentDetails(result).documents)
|
|
254
|
+
? (documentDetails(result).documents as DocumentLike[])
|
|
255
|
+
: [];
|
|
256
|
+
|
|
257
|
+
return new LinearListResultComponent(documents, theme, {
|
|
258
|
+
noun: 'document',
|
|
259
|
+
emptyLabel: 'No documents found',
|
|
260
|
+
previewLimit: DOCUMENT_LIST_PREVIEW_LIMIT,
|
|
261
|
+
renderItems: renderDocumentTable,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function renderLinearDocumentResult(actionLabel: string) {
|
|
266
|
+
return (
|
|
267
|
+
result: AgentToolResult<any>,
|
|
268
|
+
options: ToolRenderResultOptions,
|
|
269
|
+
theme: Theme,
|
|
270
|
+
context: LinearToolRenderContext,
|
|
271
|
+
): Text => {
|
|
272
|
+
if (options.isPartial) return new Text(theme.fg('warning', `${actionLabel}…`), 0, 0);
|
|
273
|
+
if (shouldShowJson(options, context)) return expandedJson(result, theme);
|
|
274
|
+
|
|
275
|
+
const document = documentDetails(result).document;
|
|
276
|
+
if (!document) {
|
|
277
|
+
return new Text(`\n${theme.fg('dim', 'Document not found')}\n\n${jsonHint()}`, 0, 0);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const metadata = metadataParts(document);
|
|
281
|
+
const snippet = documentSnippet(document);
|
|
282
|
+
const url = asString(document.url);
|
|
283
|
+
|
|
284
|
+
let text = `\n${theme.fg('success', `✓ ${actionLabel}`)} ${formatDocumentTitle(document, theme)}`;
|
|
285
|
+
if (metadata.length) text += `\n ${theme.fg('dim', metadata.join(' · '))}`;
|
|
286
|
+
if (url) text += `\n ${theme.fg('dim', url)}`;
|
|
287
|
+
if (snippet) text += `\n ${theme.fg('muted', snippet)}`;
|
|
288
|
+
text += `\n\n${jsonHint()}`;
|
|
289
|
+
|
|
290
|
+
return new Text(text, 0, 0);
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function renderLinearDocumentSuccessResult(defaultActionLabel: string) {
|
|
295
|
+
return (
|
|
296
|
+
result: AgentToolResult<any>,
|
|
297
|
+
options: ToolRenderResultOptions,
|
|
298
|
+
theme: Theme,
|
|
299
|
+
context: { args?: unknown },
|
|
300
|
+
): Text => {
|
|
301
|
+
if (options.isPartial)
|
|
302
|
+
return new Text(theme.fg('warning', `${defaultActionLabel} document…`), 0, 0);
|
|
303
|
+
if (shouldShowJson(options, context)) return expandedJson(result, theme);
|
|
304
|
+
|
|
305
|
+
const details = documentDetails(result);
|
|
306
|
+
const args = (context.args ?? {}) as ToolArgs;
|
|
307
|
+
const documentId = asString(args.documentId) ?? 'document';
|
|
308
|
+
|
|
309
|
+
if (details.success !== true) {
|
|
310
|
+
return new Text(
|
|
311
|
+
`\n${theme.fg('warning', `${defaultActionLabel} status unknown`)}\n\n${jsonHint()}`,
|
|
312
|
+
0,
|
|
313
|
+
0,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return new Text(
|
|
318
|
+
`\n${theme.fg('success', `✓ ${defaultActionLabel}`)} ${theme.fg(
|
|
319
|
+
'accent',
|
|
320
|
+
documentId,
|
|
321
|
+
)}\n\n${jsonHint()}`,
|
|
322
|
+
0,
|
|
323
|
+
0,
|
|
324
|
+
);
|
|
325
|
+
};
|
|
326
|
+
}
|
|
@@ -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
|
+
}
|