@soleone/pi-tasks 0.2.2 → 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.2",
3
+ "version": "0.3.0",
4
4
  "description": "Task management extension for the pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
@@ -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
  }
@@ -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()