@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.
- package/README.md +16 -2
- package/assets/linear_list_issues.png +0 -0
- package/assets/screenshot.png +0 -0
- package/extensions/params.ts +40 -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 +437 -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/selections.ts +10 -3
- package/extensions/settings.ts +40 -7
- package/extensions/tools/comments.ts +30 -11
- package/extensions/tools/documents.ts +42 -11
- package/extensions/tools/initiatives.ts +43 -11
- package/extensions/tools/issue-labels.ts +36 -11
- package/extensions/tools/issue-relations.ts +32 -13
- package/extensions/tools/issue-statuses.ts +19 -11
- package/extensions/tools/issues.ts +53 -19
- package/extensions/tools/milestones.ts +31 -11
- package/extensions/tools/project-labels.ts +30 -11
- package/extensions/tools/project-relations.ts +32 -13
- package/extensions/tools/projects.ts +48 -16
- package/extensions/tools/teams.ts +23 -11
- package/extensions/tools/users.ts +23 -11
- package/extensions/tools/workspaces.ts +6 -0
- package/extensions/types.ts +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,246 @@
|
|
|
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
|
+
toolOutputStyle,
|
|
17
|
+
truncate,
|
|
18
|
+
truncateLine,
|
|
19
|
+
type LinearToolRenderContext,
|
|
20
|
+
type TableColumn,
|
|
21
|
+
type ToolArgs,
|
|
22
|
+
} from './common';
|
|
23
|
+
|
|
24
|
+
type WorkflowStateLike = {
|
|
25
|
+
id?: string | null;
|
|
26
|
+
name?: string | null;
|
|
27
|
+
type?: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type TeamLike = {
|
|
31
|
+
id?: string | null;
|
|
32
|
+
key?: string | null;
|
|
33
|
+
name?: string | null;
|
|
34
|
+
description?: string | null;
|
|
35
|
+
color?: string | null;
|
|
36
|
+
icon?: string | null;
|
|
37
|
+
private?: boolean | null;
|
|
38
|
+
states?: { nodes?: WorkflowStateLike[] | null } | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type TeamResultDetails = {
|
|
42
|
+
team?: TeamLike | null;
|
|
43
|
+
teams?: TeamLike[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const TEAM_LIST_PREVIEW_LIMIT = 20;
|
|
47
|
+
const NAME_LIMIT = 90;
|
|
48
|
+
const DESCRIPTION_LIMIT = 160;
|
|
49
|
+
const TABLE_NAME_MIN_WIDTH = 24;
|
|
50
|
+
const STATE_TYPE_ORDER = ['backlog', 'unstarted', 'started', 'completed', 'canceled', 'cancelled'];
|
|
51
|
+
|
|
52
|
+
function teamDetails(result: AgentToolResult<any>): TeamResultDetails {
|
|
53
|
+
return (result.details ?? {}) as TeamResultDetails;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function teamName(team: TeamLike): string {
|
|
57
|
+
return truncate(cleanOneLine(asString(team.name) ?? '(unnamed team)'), NAME_LIMIT);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function teamKey(team: TeamLike): string {
|
|
61
|
+
return asString(team.key) ?? asString(team.id) ?? '—';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function privateText(team: TeamLike): string | undefined {
|
|
65
|
+
if (typeof team.private !== 'boolean') return undefined;
|
|
66
|
+
return team.private ? 'yes' : 'no';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stateNodes(team: TeamLike): WorkflowStateLike[] {
|
|
70
|
+
const nodes = team.states?.nodes;
|
|
71
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stateTypeRank(type: string): number {
|
|
75
|
+
const index = STATE_TYPE_ORDER.indexOf(type.toLowerCase());
|
|
76
|
+
return index === -1 ? STATE_TYPE_ORDER.length : index;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function statesSummary(team: TeamLike): string | undefined {
|
|
80
|
+
const states = stateNodes(team);
|
|
81
|
+
if (states.length === 0) return undefined;
|
|
82
|
+
|
|
83
|
+
const counts = new Map<string, number>();
|
|
84
|
+
for (const state of states) {
|
|
85
|
+
const type = asString(state.type);
|
|
86
|
+
if (!type) continue;
|
|
87
|
+
counts.set(type, (counts.get(type) ?? 0) + 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (counts.size === 0) return `${states.length} states`;
|
|
91
|
+
|
|
92
|
+
return [...counts.entries()]
|
|
93
|
+
.sort(
|
|
94
|
+
([left], [right]) => stateTypeRank(left) - stateTypeRank(right) || left.localeCompare(right),
|
|
95
|
+
)
|
|
96
|
+
.map(([type, count]) => `${type} ${count}`)
|
|
97
|
+
.join(' · ');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function descriptionSnippet(team: TeamLike): string | undefined {
|
|
101
|
+
const description = asString(team.description);
|
|
102
|
+
if (!description) return undefined;
|
|
103
|
+
return truncate(cleanOneLine(description), DESCRIPTION_LIMIT);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function metadataParts(team: TeamLike): string[] {
|
|
107
|
+
const privacy = privateText(team);
|
|
108
|
+
const states = statesSummary(team);
|
|
109
|
+
const color = asString(team.color);
|
|
110
|
+
const icon = asString(team.icon);
|
|
111
|
+
const description = descriptionSnippet(team);
|
|
112
|
+
|
|
113
|
+
return [
|
|
114
|
+
privacy ? `private: ${privacy}` : undefined,
|
|
115
|
+
states ? `states: ${states}` : undefined,
|
|
116
|
+
color ? `color: ${color}` : undefined,
|
|
117
|
+
icon ? `icon: ${icon}` : undefined,
|
|
118
|
+
description,
|
|
119
|
+
].filter((part): part is string => !!part);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatTeamListLine(team: TeamLike, theme: Theme, width: number): string {
|
|
123
|
+
const key = teamKey(team);
|
|
124
|
+
const keyPrefix = key === '—' ? '' : `${theme.fg('accent', key)} `;
|
|
125
|
+
const metadata = metadataParts(team);
|
|
126
|
+
const suffix = metadata.length ? theme.fg('dim', ` · ${metadata.join(' · ')}`) : '';
|
|
127
|
+
|
|
128
|
+
return truncateLine(` ${keyPrefix}${theme.fg('toolOutput', teamName(team))}${suffix}`, width);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function privateStyle(theme: Theme, value: string): (text: string) => string {
|
|
132
|
+
if (value === 'yes') return (text) => theme.fg('warning', text);
|
|
133
|
+
if (value === 'no' || value === '—') return (text) => theme.fg('dim', text);
|
|
134
|
+
return (text) => theme.fg('muted', text);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const TEAM_TABLE_COLUMNS: TableColumn<TeamLike>[] = [
|
|
138
|
+
{
|
|
139
|
+
id: 'key',
|
|
140
|
+
label: 'Key',
|
|
141
|
+
width: 10,
|
|
142
|
+
value: teamKey,
|
|
143
|
+
style: (theme, value) =>
|
|
144
|
+
value === '—' ? (text) => theme.fg('dim', text) : (text) => theme.fg('accent', text),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'private',
|
|
148
|
+
label: 'Private',
|
|
149
|
+
width: 8,
|
|
150
|
+
value: (team) => privateText(team) ?? '—',
|
|
151
|
+
style: privateStyle,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'states',
|
|
155
|
+
label: 'States',
|
|
156
|
+
width: 36,
|
|
157
|
+
value: (team) => statesSummary(team) ?? '—',
|
|
158
|
+
style: (theme) => (text) => theme.fg('dim', text),
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
function renderTeamTable(teams: TeamLike[], theme: Theme, width: number): string[] {
|
|
163
|
+
return renderResponsiveTable(teams, theme, width, {
|
|
164
|
+
columns: TEAM_TABLE_COLUMNS,
|
|
165
|
+
primary: {
|
|
166
|
+
label: 'Name',
|
|
167
|
+
minWidth: TABLE_NAME_MIN_WIDTH,
|
|
168
|
+
value: teamName,
|
|
169
|
+
style: (theme) => toolOutputStyle(theme),
|
|
170
|
+
},
|
|
171
|
+
dropOrder: ['states', 'private', 'key'],
|
|
172
|
+
fallback: formatTeamListLine,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function formatTeamTitle(team: TeamLike, theme: Theme): string {
|
|
177
|
+
const key = teamKey(team);
|
|
178
|
+
const prefix = key === '—' ? '' : `${theme.fg('accent', key)} `;
|
|
179
|
+
return `${prefix}${theme.fg('toolOutput', teamName(team))}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function renderTeamCard(
|
|
183
|
+
actionLabel: string,
|
|
184
|
+
team: TeamLike | null | undefined,
|
|
185
|
+
theme: Theme,
|
|
186
|
+
): Text {
|
|
187
|
+
if (!team) {
|
|
188
|
+
return new Text(`\n${theme.fg('dim', 'Team not found')}\n\n${jsonHint()}`, 0, 0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const metadata = metadataParts(team);
|
|
192
|
+
|
|
193
|
+
let text = `\n${theme.fg('success', `✓ ${actionLabel}`)} ${formatTeamTitle(team, theme)}`;
|
|
194
|
+
if (metadata.length) text += `\n ${theme.fg('dim', metadata.join(' · '))}`;
|
|
195
|
+
text += `\n\n${jsonHint()}`;
|
|
196
|
+
|
|
197
|
+
return new Text(text, 0, 0);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function renderLinearTeamListCall(args: ToolArgs | undefined, theme: Theme): Text {
|
|
201
|
+
return renderLinearToolCall('linear_list_teams', args, theme, [
|
|
202
|
+
['first', 'first'],
|
|
203
|
+
['orderBy', 'order'],
|
|
204
|
+
['filter', 'filter'],
|
|
205
|
+
['includeArchived', 'archived'],
|
|
206
|
+
]);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function renderLinearGetTeamCall(args: ToolArgs | undefined, theme: Theme): Text {
|
|
210
|
+
return renderLinearToolCall('linear_get_team', args, theme, [['teamId', 'teamId']]);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function renderLinearTeamListResult(
|
|
214
|
+
result: AgentToolResult<any>,
|
|
215
|
+
options: ToolRenderResultOptions,
|
|
216
|
+
theme: Theme,
|
|
217
|
+
context: LinearToolRenderContext,
|
|
218
|
+
): Text | LinearListResultComponent<TeamLike> {
|
|
219
|
+
if (options.isPartial) return new Text(theme.fg('warning', 'Loading teams…'), 0, 0);
|
|
220
|
+
if (shouldShowJson(options, context)) return expandedJson(result, theme);
|
|
221
|
+
|
|
222
|
+
const teams = Array.isArray(teamDetails(result).teams)
|
|
223
|
+
? (teamDetails(result).teams as TeamLike[])
|
|
224
|
+
: [];
|
|
225
|
+
|
|
226
|
+
return new LinearListResultComponent(teams, theme, {
|
|
227
|
+
noun: 'team',
|
|
228
|
+
emptyLabel: 'No teams found',
|
|
229
|
+
previewLimit: TEAM_LIST_PREVIEW_LIMIT,
|
|
230
|
+
renderItems: renderTeamTable,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function renderLinearTeamResult(actionLabel: string) {
|
|
235
|
+
return (
|
|
236
|
+
result: AgentToolResult<any>,
|
|
237
|
+
options: ToolRenderResultOptions,
|
|
238
|
+
theme: Theme,
|
|
239
|
+
context: LinearToolRenderContext,
|
|
240
|
+
): Text => {
|
|
241
|
+
if (options.isPartial) return new Text(theme.fg('warning', `${actionLabel}…`), 0, 0);
|
|
242
|
+
if (shouldShowJson(options, context)) return expandedJson(result, theme);
|
|
243
|
+
|
|
244
|
+
return renderTeamCard(actionLabel, teamDetails(result).team, theme);
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
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 UserLike = {
|
|
27
|
+
id?: string | null;
|
|
28
|
+
name?: string | null;
|
|
29
|
+
displayName?: string | null;
|
|
30
|
+
email?: string | null;
|
|
31
|
+
active?: boolean | null;
|
|
32
|
+
admin?: boolean | null;
|
|
33
|
+
guest?: boolean | null;
|
|
34
|
+
isAssignable?: boolean | null;
|
|
35
|
+
createdAt?: string | null;
|
|
36
|
+
updatedAt?: string | null;
|
|
37
|
+
url?: string | null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type UserResultDetails = {
|
|
41
|
+
user?: UserLike | null;
|
|
42
|
+
users?: UserLike[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const USER_LIST_PREVIEW_LIMIT = 20;
|
|
46
|
+
const NAME_LIMIT = 90;
|
|
47
|
+
const TABLE_NAME_MIN_WIDTH = 24;
|
|
48
|
+
|
|
49
|
+
function userDetails(result: AgentToolResult<any>): UserResultDetails {
|
|
50
|
+
return (result.details ?? {}) as UserResultDetails;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function userName(user: UserLike): string {
|
|
54
|
+
return truncate(
|
|
55
|
+
cleanOneLine(asString(user.name) ?? asString(user.displayName) ?? '(unnamed user)'),
|
|
56
|
+
NAME_LIMIT,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function displayNameText(user: UserLike): string | undefined {
|
|
61
|
+
const displayName = asString(user.displayName);
|
|
62
|
+
const name = asString(user.name);
|
|
63
|
+
if (!displayName || !name || displayName === name) return undefined;
|
|
64
|
+
return displayName;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function activeText(user: UserLike): string {
|
|
68
|
+
if (user.active === true) return 'active';
|
|
69
|
+
if (user.active === false) return 'disabled';
|
|
70
|
+
return '—';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function roleText(user: UserLike): string {
|
|
74
|
+
if (user.admin === true) return 'admin';
|
|
75
|
+
if (user.guest === true) return 'guest';
|
|
76
|
+
return 'member';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function assignableText(user: UserLike): string {
|
|
80
|
+
if (user.isAssignable === true) return 'yes';
|
|
81
|
+
if (user.isAssignable === false) return 'no';
|
|
82
|
+
return '—';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function metadataParts(user: UserLike): string[] {
|
|
86
|
+
const displayName = displayNameText(user);
|
|
87
|
+
const email = asString(user.email);
|
|
88
|
+
|
|
89
|
+
return [
|
|
90
|
+
displayName ? `display: ${displayName}` : undefined,
|
|
91
|
+
email,
|
|
92
|
+
activeText(user),
|
|
93
|
+
roleText(user),
|
|
94
|
+
`assignable: ${assignableText(user)}`,
|
|
95
|
+
].filter((part): part is string => !!part);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatUserListLine(user: UserLike, theme: Theme, width: number): string {
|
|
99
|
+
const metadata = metadataParts(user);
|
|
100
|
+
const suffix = metadata.length ? theme.fg('dim', ` · ${metadata.join(' · ')}`) : '';
|
|
101
|
+
|
|
102
|
+
return truncateLine(` ${theme.fg('toolOutput', userName(user))}${suffix}`, width);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function activeStyle(theme: Theme, value: string): (text: string) => string {
|
|
106
|
+
if (value === 'active') return (text) => theme.fg('success', text);
|
|
107
|
+
if (value === 'disabled') return dimStyle(theme);
|
|
108
|
+
return mutedStyle(theme);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function roleStyle(theme: Theme, value: string): (text: string) => string {
|
|
112
|
+
if (value === 'admin') return (text) => theme.fg('warning', text);
|
|
113
|
+
if (value === 'guest') return mutedStyle(theme);
|
|
114
|
+
return dimStyle(theme);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function assignableStyle(theme: Theme, value: string): (text: string) => string {
|
|
118
|
+
if (value === 'yes') return (text) => theme.fg('success', text);
|
|
119
|
+
if (value === 'no') return dimStyle(theme);
|
|
120
|
+
return mutedStyle(theme);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const USER_TABLE_COLUMNS: TableColumn<UserLike>[] = [
|
|
124
|
+
{
|
|
125
|
+
id: 'email',
|
|
126
|
+
label: 'Email',
|
|
127
|
+
width: 30,
|
|
128
|
+
value: (user) => asString(user.email) ?? '—',
|
|
129
|
+
style: (theme) => mutedStyle(theme),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'active',
|
|
133
|
+
label: 'Active',
|
|
134
|
+
width: 8,
|
|
135
|
+
value: activeText,
|
|
136
|
+
style: activeStyle,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'role',
|
|
140
|
+
label: 'Role',
|
|
141
|
+
width: 7,
|
|
142
|
+
value: roleText,
|
|
143
|
+
style: roleStyle,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'assignable',
|
|
147
|
+
label: 'Assignable',
|
|
148
|
+
width: 10,
|
|
149
|
+
value: assignableText,
|
|
150
|
+
style: assignableStyle,
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
function renderUserTable(users: UserLike[], theme: Theme, width: number): string[] {
|
|
155
|
+
return renderResponsiveTable(users, theme, width, {
|
|
156
|
+
columns: USER_TABLE_COLUMNS,
|
|
157
|
+
primary: {
|
|
158
|
+
label: 'Name',
|
|
159
|
+
minWidth: TABLE_NAME_MIN_WIDTH,
|
|
160
|
+
value: userName,
|
|
161
|
+
style: (theme) => toolOutputStyle(theme),
|
|
162
|
+
},
|
|
163
|
+
dropOrder: ['assignable', 'role', 'active', 'email'],
|
|
164
|
+
fallback: formatUserListLine,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function formatUserTitle(user: UserLike, theme: Theme): string {
|
|
169
|
+
const displayName = displayNameText(user);
|
|
170
|
+
const name = userName(user);
|
|
171
|
+
const title = displayName ? `${name} (${truncate(cleanOneLine(displayName), NAME_LIMIT)})` : name;
|
|
172
|
+
return theme.fg('toolOutput', title);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderUserCard(user: UserLike | null | undefined, theme: Theme): Text {
|
|
176
|
+
if (!user) {
|
|
177
|
+
return new Text(`\n${theme.fg('dim', 'User not found')}\n\n${jsonHint()}`, 0, 0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const email = asString(user.email);
|
|
181
|
+
const url = asString(user.url);
|
|
182
|
+
const metadata = [
|
|
183
|
+
email,
|
|
184
|
+
activeText(user),
|
|
185
|
+
roleText(user),
|
|
186
|
+
`assignable: ${assignableText(user)}`,
|
|
187
|
+
].filter((part): part is string => !!part);
|
|
188
|
+
|
|
189
|
+
let text = `\n${theme.fg('success', '✓ User')} ${formatUserTitle(user, theme)}`;
|
|
190
|
+
if (metadata.length) text += `\n ${theme.fg('dim', metadata.join(' · '))}`;
|
|
191
|
+
if (url) text += `\n ${theme.fg('dim', url)}`;
|
|
192
|
+
text += `\n\n${jsonHint()}`;
|
|
193
|
+
|
|
194
|
+
return new Text(text, 0, 0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function renderLinearUserListCall(args: ToolArgs | undefined, theme: Theme): Text {
|
|
198
|
+
return renderLinearToolCall('linear_list_users', args, theme, [
|
|
199
|
+
['first', 'first'],
|
|
200
|
+
['orderBy', 'order'],
|
|
201
|
+
['filter', 'filter'],
|
|
202
|
+
['includeArchived', 'includeArchived'],
|
|
203
|
+
['includeDisabled', 'includeDisabled'],
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function renderLinearGetUserCall(args: ToolArgs | undefined, theme: Theme): Text {
|
|
208
|
+
return renderLinearToolCall('linear_get_user', args, theme, [['userId', 'userId']]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function renderLinearUserListResult(
|
|
212
|
+
result: AgentToolResult<any>,
|
|
213
|
+
options: ToolRenderResultOptions,
|
|
214
|
+
theme: Theme,
|
|
215
|
+
context: LinearToolRenderContext,
|
|
216
|
+
): Text | LinearListResultComponent<UserLike> {
|
|
217
|
+
if (options.isPartial) return new Text(theme.fg('warning', 'Loading users…'), 0, 0);
|
|
218
|
+
if (shouldShowJson(options, context)) return expandedJson(result, theme);
|
|
219
|
+
|
|
220
|
+
const users = Array.isArray(userDetails(result).users)
|
|
221
|
+
? (userDetails(result).users as UserLike[])
|
|
222
|
+
: [];
|
|
223
|
+
|
|
224
|
+
return new LinearListResultComponent(users, theme, {
|
|
225
|
+
noun: 'user',
|
|
226
|
+
emptyLabel: 'No users found',
|
|
227
|
+
previewLimit: USER_LIST_PREVIEW_LIMIT,
|
|
228
|
+
renderItems: renderUserTable,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function renderLinearUserResult(
|
|
233
|
+
result: AgentToolResult<any>,
|
|
234
|
+
options: ToolRenderResultOptions,
|
|
235
|
+
theme: Theme,
|
|
236
|
+
context: LinearToolRenderContext,
|
|
237
|
+
): Text {
|
|
238
|
+
if (options.isPartial) return new Text(theme.fg('warning', 'Loading user…'), 0, 0);
|
|
239
|
+
if (shouldShowJson(options, context)) return expandedJson(result, theme);
|
|
240
|
+
|
|
241
|
+
return renderUserCard(userDetails(result).user, theme);
|
|
242
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
expandedJson,
|
|
10
|
+
shouldShowJson,
|
|
11
|
+
jsonHint,
|
|
12
|
+
renderLinearToolCall,
|
|
13
|
+
type LinearToolRenderContext,
|
|
14
|
+
type ToolArgs,
|
|
15
|
+
} from './common';
|
|
16
|
+
|
|
17
|
+
type WorkspaceSwitchResultDetails = {
|
|
18
|
+
active?: string | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function workspaceSwitchDetails(result: AgentToolResult<any>): WorkspaceSwitchResultDetails {
|
|
22
|
+
return (result.details ?? {}) as WorkspaceSwitchResultDetails;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function renderLinearSwitchWorkspaceCall(args: ToolArgs | undefined, theme: Theme): Text {
|
|
26
|
+
return renderLinearToolCall('linear_switch_workspace', args, theme, [['name', 'name']]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function renderLinearSwitchWorkspaceResult(
|
|
30
|
+
result: AgentToolResult<any>,
|
|
31
|
+
options: ToolRenderResultOptions,
|
|
32
|
+
theme: Theme,
|
|
33
|
+
context: LinearToolRenderContext,
|
|
34
|
+
): Text {
|
|
35
|
+
if (options.isPartial) return new Text(theme.fg('warning', 'Switching workspace…'), 0, 0);
|
|
36
|
+
if (shouldShowJson(options, context)) return expandedJson(result, theme);
|
|
37
|
+
|
|
38
|
+
const active = asString(workspaceSwitchDetails(result).active) ?? 'unknown';
|
|
39
|
+
return new Text(
|
|
40
|
+
`\n${theme.fg('success', `✓ Active Linear workspace: ${active}`)}\n\n${jsonHint()}`,
|
|
41
|
+
0,
|
|
42
|
+
0,
|
|
43
|
+
);
|
|
44
|
+
}
|
package/extensions/selections.ts
CHANGED
|
@@ -94,7 +94,7 @@ export const ISSUE_LABEL_SELECTION = `
|
|
|
94
94
|
}
|
|
95
95
|
`;
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
const PROJECT_BASE_SELECTION = `
|
|
98
98
|
id
|
|
99
99
|
name
|
|
100
100
|
description
|
|
@@ -116,7 +116,7 @@ export const PROJECT_SELECTION = `
|
|
|
116
116
|
createdAt
|
|
117
117
|
updatedAt
|
|
118
118
|
url
|
|
119
|
-
teams {
|
|
119
|
+
teams(first: 10) {
|
|
120
120
|
nodes {
|
|
121
121
|
id
|
|
122
122
|
key
|
|
@@ -128,7 +128,7 @@ export const PROJECT_SELECTION = `
|
|
|
128
128
|
name
|
|
129
129
|
email
|
|
130
130
|
}
|
|
131
|
-
members {
|
|
131
|
+
members(first: 10) {
|
|
132
132
|
nodes {
|
|
133
133
|
id
|
|
134
134
|
name
|
|
@@ -141,6 +141,13 @@ export const PROJECT_SELECTION = `
|
|
|
141
141
|
}
|
|
142
142
|
`;
|
|
143
143
|
|
|
144
|
+
export const PROJECT_LIST_SELECTION = PROJECT_BASE_SELECTION;
|
|
145
|
+
|
|
146
|
+
export const PROJECT_DETAIL_SELECTION = `
|
|
147
|
+
${PROJECT_BASE_SELECTION}
|
|
148
|
+
content
|
|
149
|
+
`;
|
|
150
|
+
|
|
144
151
|
export const PROJECT_LABEL_SELECTION = `
|
|
145
152
|
id
|
|
146
153
|
name
|
package/extensions/settings.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, join } from 'node:path';
|
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext } from '@mariozechner/pi-coding-agent';
|
|
4
4
|
import { getAgentDir, getSettingsListTheme } from '@mariozechner/pi-coding-agent';
|
|
5
5
|
import { type SettingItem, SettingsList } from '@mariozechner/pi-tui';
|
|
6
|
+
import { invalidateLinearResultRenderers, setDefaultJsonView } from './renderers/state';
|
|
6
7
|
|
|
7
8
|
const SETTINGS_PATH = join(getAgentDir(), 'state', 'extensions', 'linear', 'tool-settings.json');
|
|
8
9
|
const OVERLAY_MAX_INNER = 60;
|
|
@@ -166,28 +167,37 @@ const ALL_LINEAR_TOOLS = TOOL_CATEGORIES.flatMap((c) => c.tools);
|
|
|
166
167
|
|
|
167
168
|
type ToolSettings = {
|
|
168
169
|
disabledTools: string[];
|
|
170
|
+
defaultJsonView: boolean;
|
|
169
171
|
};
|
|
170
172
|
|
|
171
173
|
function createDefaultSettings(): ToolSettings {
|
|
172
|
-
return { disabledTools: [] };
|
|
174
|
+
return { disabledTools: [], defaultJsonView: false };
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
async function loadSettings(): Promise<ToolSettings> {
|
|
176
178
|
try {
|
|
177
179
|
const raw = JSON.parse(await fs.readFile(SETTINGS_PATH, 'utf8'));
|
|
178
180
|
if (!raw || typeof raw !== 'object' || !Array.isArray(raw.disabledTools)) {
|
|
179
|
-
|
|
181
|
+
const settings = createDefaultSettings();
|
|
182
|
+
setDefaultJsonView(settings.defaultJsonView);
|
|
183
|
+
return settings;
|
|
180
184
|
}
|
|
181
|
-
|
|
185
|
+
const settings = {
|
|
182
186
|
disabledTools: raw.disabledTools.filter((t: unknown) => typeof t === 'string'),
|
|
187
|
+
defaultJsonView: typeof raw.defaultJsonView === 'boolean' ? raw.defaultJsonView : false,
|
|
183
188
|
};
|
|
189
|
+
setDefaultJsonView(settings.defaultJsonView);
|
|
190
|
+
return settings;
|
|
184
191
|
} catch {
|
|
185
|
-
|
|
192
|
+
const settings = createDefaultSettings();
|
|
193
|
+
setDefaultJsonView(settings.defaultJsonView);
|
|
194
|
+
return settings;
|
|
186
195
|
}
|
|
187
196
|
}
|
|
188
197
|
|
|
189
198
|
async function saveSettings(settings: ToolSettings): Promise<boolean> {
|
|
190
199
|
try {
|
|
200
|
+
setDefaultJsonView(settings.defaultJsonView);
|
|
191
201
|
await fs.mkdir(dirname(SETTINGS_PATH), { recursive: true });
|
|
192
202
|
await fs.writeFile(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
193
203
|
return true;
|
|
@@ -256,8 +266,21 @@ function frameBody(title: string, bodyLines: string[], inner: number): string[]
|
|
|
256
266
|
return [top, ...framedBody, bottom];
|
|
257
267
|
}
|
|
258
268
|
|
|
269
|
+
function defaultOutputViewValue(settings: ToolSettings): string {
|
|
270
|
+
return settings.defaultJsonView ? 'Full JSON' : 'Human readable';
|
|
271
|
+
}
|
|
272
|
+
|
|
259
273
|
function buildItems(settings: ToolSettings): SettingItem[] {
|
|
260
|
-
const items: SettingItem[] = [
|
|
274
|
+
const items: SettingItem[] = [
|
|
275
|
+
{
|
|
276
|
+
id: 'defaultJsonView',
|
|
277
|
+
label: 'Default output view',
|
|
278
|
+
description:
|
|
279
|
+
'Controls Linear tool result display. Ctrl+O toggles the other view per tool call.',
|
|
280
|
+
currentValue: defaultOutputViewValue(settings),
|
|
281
|
+
values: ['Human readable', 'Full JSON'],
|
|
282
|
+
},
|
|
283
|
+
];
|
|
261
284
|
for (const category of TOOL_CATEGORIES) {
|
|
262
285
|
items.push({
|
|
263
286
|
id: `category:${category.id}`,
|
|
@@ -308,7 +331,7 @@ async function showToolSettingsOverlay(
|
|
|
308
331
|
() => {},
|
|
309
332
|
);
|
|
310
333
|
const probeLines = probeList.render(Math.max(8, OVERLAY_MAX_INNER - 2));
|
|
311
|
-
const overlayBodyLines = ['
|
|
334
|
+
const overlayBodyLines = ['Configure Linear output and enabled tools', '', ...probeLines];
|
|
312
335
|
const overlayWidth = computeOverlayInner(overlayBodyLines, OVERLAY_MAX_INNER + 2) + 2;
|
|
313
336
|
|
|
314
337
|
await ctx.ui.custom(
|
|
@@ -318,6 +341,16 @@ async function showToolSettingsOverlay(
|
|
|
318
341
|
maxVisibleItems,
|
|
319
342
|
settingsTheme,
|
|
320
343
|
async (id, newValue) => {
|
|
344
|
+
if (id === 'defaultJsonView') {
|
|
345
|
+
const previousDefaultJsonView = settings.defaultJsonView;
|
|
346
|
+
settings.defaultJsonView = newValue === 'Full JSON';
|
|
347
|
+
await saveSettings(settings);
|
|
348
|
+
if (settings.defaultJsonView !== previousDefaultJsonView) {
|
|
349
|
+
invalidateLinearResultRenderers();
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
321
354
|
const nextEnabled = newValue === '[x]';
|
|
322
355
|
|
|
323
356
|
if (id.startsWith('category:')) {
|
|
@@ -364,7 +397,7 @@ async function showToolSettingsOverlay(
|
|
|
364
397
|
const provisionalInner = Math.max(24, Math.min(safeWidth - 2, OVERLAY_MAX_INNER));
|
|
365
398
|
const listLines = settingsList.render(Math.max(8, provisionalInner - 2));
|
|
366
399
|
const bodyLines = [
|
|
367
|
-
theme.fg('muted', '
|
|
400
|
+
theme.fg('muted', 'Configure Linear output and enabled tools'),
|
|
368
401
|
'',
|
|
369
402
|
...listLines,
|
|
370
403
|
];
|