@soleone/pi-tasks 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soleone/pi-tasks",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Task management extension for the pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@ export interface ListControllerState {
23
23
  filtered: boolean
24
24
  allowSearch: boolean
25
25
  allowPriority: boolean
26
- ctrlQ: string
26
+ closeKey: string
27
27
  priorities: string[]
28
28
  priorityHotkeys?: Record<string, string>
29
29
  }
@@ -83,7 +83,7 @@ const SCROLL_KEYS: Record<string, number> = {
83
83
  const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
84
84
  {
85
85
  context: "search",
86
- match: (data, state) => data === state.ctrlQ,
86
+ match: (data, state) => data === state.closeKey,
87
87
  intent: () => ({ type: "cancel" }),
88
88
  },
89
89
  {
@@ -174,7 +174,7 @@ const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
174
174
  },
175
175
  {
176
176
  context: "default",
177
- match: (data, state) => data === state.ctrlQ,
177
+ match: (data, state) => data === state.closeKey,
178
178
  intent: () => ({ type: "cancel" }),
179
179
  },
180
180
  ]
package/src/extension.ts CHANGED
@@ -6,7 +6,7 @@ import { showTaskList } from "./ui/pages/list.ts"
6
6
  import { showTaskForm } from "./ui/pages/show.ts"
7
7
  import type { TaskUpdate } from "./backend/api.ts"
8
8
 
9
- const CTRL_Q = "\x11"
9
+ const CTRL_X = "\x18"
10
10
 
11
11
  function parsePriorityKey(
12
12
  data: string,
@@ -212,7 +212,7 @@ export default function registerExtension(pi: ExtensionAPI) {
212
212
  mode: "edit",
213
213
  subtitle: "Edit",
214
214
  task,
215
- ctrlQ: CTRL_Q,
215
+ closeKey: CTRL_X,
216
216
  cycleStatus: nextStatus,
217
217
  cycleTaskType: nextTaskType,
218
218
  parsePriorityKey: nextPriorityFromKey,
@@ -261,7 +261,7 @@ export default function registerExtension(pi: ExtensionAPI) {
261
261
  priority: defaultPriority(backend.priorities),
262
262
  taskType: defaultTaskType(backend.taskTypes),
263
263
  },
264
- ctrlQ: CTRL_Q,
264
+ closeKey: CTRL_X,
265
265
  cycleStatus: nextStatus,
266
266
  cycleTaskType: nextTaskType,
267
267
  parsePriorityKey: nextPriorityFromKey,
@@ -323,7 +323,7 @@ export default function registerExtension(pi: ExtensionAPI) {
323
323
  title: pageTitle,
324
324
  subtitle: backendLabel,
325
325
  tasks,
326
- ctrlQ: CTRL_Q,
326
+ closeKey: CTRL_X,
327
327
  priorities: backend.priorities,
328
328
  priorityHotkeys: backend.priorityHotkeys,
329
329
  cycleStatus: nextStatus,
@@ -0,0 +1,153 @@
1
+ import { Key, matchesKey, truncateToWidth, type Component, type SelectItem, type SelectListTheme } from "@mariozechner/pi-tui"
2
+
3
+ // Local variant of pi-tui SelectList with configurable value/description column layout.
4
+
5
+ const normalizeToSingleLine = (text: string): string => text.replace(/[\r\n]+/g, " ").trim()
6
+
7
+ export interface SelectListColumnLayout {
8
+ valueMaxWidth?: number
9
+ valueColumnWidth?: number
10
+ minDescriptionWidth?: number
11
+ minWidthForDescription?: number
12
+ }
13
+
14
+ interface ResolvedSelectListColumnLayout {
15
+ valueMaxWidth: number
16
+ valueColumnWidth: number
17
+ minDescriptionWidth: number
18
+ minWidthForDescription: number
19
+ }
20
+
21
+ const DEFAULT_COLUMN_LAYOUT: ResolvedSelectListColumnLayout = {
22
+ valueMaxWidth: 30,
23
+ valueColumnWidth: 32,
24
+ minDescriptionWidth: 10,
25
+ minWidthForDescription: 40,
26
+ }
27
+
28
+ export class SelectListWithColumns implements Component {
29
+ private items: SelectItem[] = []
30
+ private filteredItems: SelectItem[] = []
31
+ private selectedIndex = 0
32
+ private maxVisible = 5
33
+ private theme: SelectListTheme
34
+ private layout: ResolvedSelectListColumnLayout
35
+
36
+ public onSelect?: (item: SelectItem) => void
37
+ public onCancel?: () => void
38
+ public onSelectionChange?: (item: SelectItem) => void
39
+
40
+ constructor(items: SelectItem[], maxVisible: number, theme: SelectListTheme, layout: SelectListColumnLayout = {}) {
41
+ this.items = items
42
+ this.filteredItems = items
43
+ this.maxVisible = maxVisible
44
+ this.theme = theme
45
+ this.layout = {
46
+ ...DEFAULT_COLUMN_LAYOUT,
47
+ ...layout,
48
+ }
49
+ }
50
+
51
+ setSelectedIndex(index: number): void {
52
+ this.selectedIndex = Math.max(0, Math.min(index, this.filteredItems.length - 1))
53
+ }
54
+
55
+ invalidate(): void {
56
+ // No cached state.
57
+ }
58
+
59
+ render(width: number): string[] {
60
+ const lines: string[] = []
61
+
62
+ if (this.filteredItems.length === 0) {
63
+ lines.push(this.theme.noMatch(" No matching commands"))
64
+ return lines
65
+ }
66
+
67
+ const startIndex = Math.max(
68
+ 0,
69
+ Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible),
70
+ )
71
+ const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length)
72
+
73
+ for (let i = startIndex; i < endIndex; i++) {
74
+ const item = this.filteredItems[i]
75
+ if (!item) continue
76
+
77
+ const isSelected = i === this.selectedIndex
78
+ const descriptionSingleLine = item.description ? normalizeToSingleLine(item.description) : undefined
79
+ const displayValue = item.label || item.value
80
+ const prefix = isSelected ? "→ " : " "
81
+
82
+ if (!descriptionSingleLine || width <= this.layout.minWidthForDescription) {
83
+ lines.push(this.renderValueOnlyLine(prefix, displayValue, width, isSelected))
84
+ continue
85
+ }
86
+
87
+ const maxValueWidth = Math.min(this.layout.valueMaxWidth, width - prefix.length - 4)
88
+ const truncatedValue = truncateToWidth(displayValue, maxValueWidth, "")
89
+ const spacing = " ".repeat(Math.max(1, this.layout.valueColumnWidth - truncatedValue.length))
90
+ const descriptionStart = prefix.length + truncatedValue.length + spacing.length
91
+ const descriptionWidth = width - descriptionStart - 2
92
+
93
+ if (descriptionWidth <= this.layout.minDescriptionWidth) {
94
+ lines.push(this.renderValueOnlyLine(prefix, displayValue, width, isSelected))
95
+ continue
96
+ }
97
+
98
+ const truncatedDesc = truncateToWidth(descriptionSingleLine, descriptionWidth, "")
99
+ if (isSelected) {
100
+ lines.push(this.theme.selectedText(`${prefix}${truncatedValue}${spacing}${truncatedDesc}`))
101
+ } else {
102
+ lines.push(`${prefix}${truncatedValue}${this.theme.description(spacing + truncatedDesc)}`)
103
+ }
104
+ }
105
+
106
+ if (startIndex > 0 || endIndex < this.filteredItems.length) {
107
+ const scrollText = ` (${this.selectedIndex + 1}/${this.filteredItems.length})`
108
+ lines.push(this.theme.scrollInfo(truncateToWidth(scrollText, width - 2, "")))
109
+ }
110
+
111
+ return lines
112
+ }
113
+
114
+ handleInput(keyData: string): void {
115
+ if (matchesKey(keyData, Key.up)) {
116
+ this.selectedIndex = this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1
117
+ this.notifySelectionChange()
118
+ return
119
+ }
120
+
121
+ if (matchesKey(keyData, Key.down)) {
122
+ this.selectedIndex = this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1
123
+ this.notifySelectionChange()
124
+ return
125
+ }
126
+
127
+ if (matchesKey(keyData, Key.enter)) {
128
+ const selectedItem = this.filteredItems[this.selectedIndex]
129
+ if (selectedItem && this.onSelect) this.onSelect(selectedItem)
130
+ return
131
+ }
132
+
133
+ if (matchesKey(keyData, Key.escape) || keyData === "\u0003") {
134
+ if (this.onCancel) this.onCancel()
135
+ }
136
+ }
137
+
138
+ getSelectedItem(): SelectItem | null {
139
+ const item = this.filteredItems[this.selectedIndex]
140
+ return item || null
141
+ }
142
+
143
+ private renderValueOnlyLine(prefix: string, displayValue: string, width: number, isSelected: boolean): string {
144
+ const maxWidth = width - prefix.length - 2
145
+ const line = `${prefix}${truncateToWidth(displayValue, maxWidth, "")}`
146
+ return isSelected ? this.theme.selectedText(line) : line
147
+ }
148
+
149
+ private notifySelectionChange(): void {
150
+ const selectedItem = this.filteredItems[this.selectedIndex]
151
+ if (selectedItem && this.onSelectionChange) this.onSelectionChange(selectedItem)
152
+ }
153
+ }
@@ -1,13 +1,18 @@
1
1
  import { DynamicBorder, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent"
2
- import { Container, SelectList, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui"
2
+ import { Container, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui"
3
3
  import { toKebabCase, type Task, type TaskStatus } from "../../models/task.ts"
4
4
  import type { TaskUpdate } from "../../backend/api.ts"
5
5
  import { DESCRIPTION_PART_SEPARATOR, buildListRowModel, decodeDescription, stripAnsi } from "../../models/list-item.ts"
6
6
  import { buildListPrimaryHelpText, buildListSecondaryHelpText, resolveListIntent } from "../../controllers/list.ts"
7
7
  import { KEYBOARD_HELP_PADDING_X, formatKeyboardHelp } from "../components/keyboard-help.ts"
8
8
  import { MinHeightContainer } from "../components/min-height.ts"
9
+ import { SelectListWithColumns } from "../components/select-list-with-columns.ts"
9
10
 
10
11
  const LIST_PAGE_CONTENT_MIN_HEIGHT = 20
12
+ const TASK_LIST_ROW_LAYOUT = {
13
+ valueMaxWidth: 60,
14
+ valueColumnWidth: 62,
15
+ }
11
16
 
12
17
  export interface ListPageConfig {
13
18
  title: string
@@ -18,7 +23,7 @@ export interface ListPageConfig {
18
23
  filterTerm?: string
19
24
  priorities: string[]
20
25
  priorityHotkeys?: Record<string, string>
21
- ctrlQ: string
26
+ closeKey: string
22
27
  cycleStatus: (status: TaskStatus) => TaskStatus
23
28
  cycleTaskType: (current: string | undefined) => string
24
29
  onUpdateTask: (ref: string, update: TaskUpdate) => Promise<void>
@@ -134,14 +139,16 @@ export async function showTaskList(ctx: ExtensionCommandContext, config: ListPag
134
139
  })
135
140
  }
136
141
 
137
- let items = getItems()
138
- let selectList = new SelectList(items, Math.min(items.length, 10), {
142
+ const selectListTheme = {
139
143
  selectedPrefix: (t: string) => theme.fg("accent", t),
140
144
  selectedText: (t: string) => applyAccentWithAnsi(t),
141
145
  description: (t: string) => styleDescription(t),
142
146
  scrollInfo: (t: string) => theme.fg("dim", t),
143
147
  noMatch: (t: string) => theme.fg("warning", t),
144
- })
148
+ }
149
+
150
+ let items = getItems()
151
+ let selectList = new SelectListWithColumns(items, Math.min(items.length, 10), selectListTheme, TASK_LIST_ROW_LAYOUT)
145
152
 
146
153
  if (rememberedSelectedRef) {
147
154
  const rememberedIndex = items.findIndex(i => i.value === rememberedSelectedRef)
@@ -165,13 +172,7 @@ export async function showTaskList(ctx: ExtensionCommandContext, config: ListPag
165
172
  selectList.onCancel = () => {
166
173
  if (filterTerm) {
167
174
  filterTerm = ""
168
- items = getItems()
169
- selectList = new SelectList(items, Math.min(items.length, 10), selectList.theme)
170
- selectList.onSelectionChange = selectList.onSelectionChange
171
- selectList.onSelect = selectList.onSelect
172
- selectList.onCancel = selectList.onCancel
173
- container.invalidate()
174
- tui.requestRender()
175
+ rebuildAndRender()
175
176
  } else {
176
177
  done("cancel")
177
178
  }
@@ -289,7 +290,7 @@ export async function showTaskList(ctx: ExtensionCommandContext, config: ListPag
289
290
  filtered: !!filterTerm,
290
291
  allowPriority,
291
292
  allowSearch,
292
- ctrlQ: config.ctrlQ,
293
+ closeKey: config.closeKey,
293
294
  priorities: config.priorities,
294
295
  priorityHotkeys: config.priorityHotkeys,
295
296
  })))
@@ -325,13 +326,7 @@ export async function showTaskList(ctx: ExtensionCommandContext, config: ListPag
325
326
  items = getItems()
326
327
  const prevSelected = selectList.getSelectedItem()
327
328
 
328
- selectList = new SelectList(items, Math.min(items.length, 10), {
329
- selectedPrefix: (t: string) => theme.fg("accent", t),
330
- selectedText: (t: string) => applyAccentWithAnsi(t),
331
- description: (t: string) => styleDescription(t),
332
- scrollInfo: (t: string) => theme.fg("dim", t),
333
- noMatch: (t: string) => theme.fg("warning", t),
334
- })
329
+ selectList = new SelectListWithColumns(items, Math.min(items.length, 10), selectListTheme, TASK_LIST_ROW_LAYOUT)
335
330
 
336
331
  selectList.onSelectionChange = () => {
337
332
  const selected = selectList.getSelectedItem()
@@ -381,7 +376,7 @@ export async function showTaskList(ctx: ExtensionCommandContext, config: ListPag
381
376
  filtered: !!filterTerm,
382
377
  allowSearch,
383
378
  allowPriority,
384
- ctrlQ: config.ctrlQ,
379
+ closeKey: config.closeKey,
385
380
  priorities: config.priorities,
386
381
  priorityHotkeys: config.priorityHotkeys,
387
382
  })
@@ -26,7 +26,7 @@ interface ShowTaskFormOptions {
26
26
  mode: FormMode
27
27
  subtitle: string
28
28
  task: Task
29
- ctrlQ: string
29
+ closeKey: string
30
30
  cycleStatus: (status: TaskStatus) => TaskStatus
31
31
  cycleTaskType: (taskType: string | undefined) => string
32
32
  parsePriorityKey: (data: string) => string | null
@@ -144,7 +144,7 @@ class ReservedLineText implements Component {
144
144
  }
145
145
 
146
146
  export async function showTaskForm(ctx: ExtensionCommandContext, options: ShowTaskFormOptions): Promise<TaskFormResult> {
147
- const { mode, subtitle, task, ctrlQ, cycleStatus, cycleTaskType, parsePriorityKey, priorities, priorityHotkeys, onSave } = options
147
+ const { mode, subtitle, task, closeKey, cycleStatus, cycleTaskType, parsePriorityKey, priorities, priorityHotkeys, onSave } = options
148
148
 
149
149
  let taskTypeValue = task.taskType
150
150
  let titleValue = task.title
@@ -397,7 +397,7 @@ export async function showTaskForm(ctx: ExtensionCommandContext, options: ShowTa
397
397
  if (saveIndicatorTimer) clearTimeout(saveIndicatorTimer)
398
398
  },
399
399
  handleInput: (data: string) => {
400
- if (data === ctrlQ) {
400
+ if (data === closeKey) {
401
401
  done({ action: "close_list" })
402
402
  return
403
403
  }