@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,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
+ }
@@ -94,7 +94,7 @@ export const ISSUE_LABEL_SELECTION = `
94
94
  }
95
95
  `;
96
96
 
97
- export const PROJECT_SELECTION = `
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
@@ -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
- return createDefaultSettings();
181
+ const settings = createDefaultSettings();
182
+ setDefaultJsonView(settings.defaultJsonView);
183
+ return settings;
180
184
  }
181
- return {
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
- return createDefaultSettings();
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 = ['Toggle Linear tools by category or individually', '', ...probeLines];
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', 'Toggle Linear tools by category or individually'),
400
+ theme.fg('muted', 'Configure Linear output and enabled tools'),
368
401
  '',
369
402
  ...listLines,
370
403
  ];