@damper/cli 0.7.2 → 0.8.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/dist/commands/start.js +19 -0
- package/dist/services/claude.d.ts +1 -0
- package/dist/services/claude.js +66 -39
- package/dist/ui/format.d.ts +13 -0
- package/dist/ui/format.js +52 -1
- package/dist/ui/task-picker.js +74 -24
- package/package.json +1 -1
package/dist/commands/start.js
CHANGED
|
@@ -100,6 +100,24 @@ export async function startCommand(options) {
|
|
|
100
100
|
// Resume existing worktree
|
|
101
101
|
console.log(pc.green(`\n✓ Resuming: #${shortIdRaw(taskId)} ${taskTitle}`));
|
|
102
102
|
console.log(pc.dim(` Worktree: ${worktreePath}`));
|
|
103
|
+
// Re-lock the task if it was previously released
|
|
104
|
+
const task = await api.getTask(taskId);
|
|
105
|
+
if (task.status !== 'in_progress' || !task.lockedBy) {
|
|
106
|
+
console.log(pc.dim('\nRe-locking task in Damper...'));
|
|
107
|
+
try {
|
|
108
|
+
await api.startTask(taskId, forceTakeover);
|
|
109
|
+
console.log(pc.green('✓ Task locked'));
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const error = err;
|
|
113
|
+
if (error.lockInfo) {
|
|
114
|
+
console.log(pc.yellow(`\n⚠️ Task is locked by "${error.lockInfo.lockedBy}" since ${error.lockInfo.lockedAt}`));
|
|
115
|
+
console.log(pc.dim('Use --force to take over the lock.\n'));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
103
121
|
// Refresh context with latest from Damper
|
|
104
122
|
console.log(pc.dim('\nRefreshing context from Damper...'));
|
|
105
123
|
await refreshContext({
|
|
@@ -165,6 +183,7 @@ export async function startCommand(options) {
|
|
|
165
183
|
taskTitle,
|
|
166
184
|
apiKey,
|
|
167
185
|
yolo: options.yolo,
|
|
186
|
+
isResume,
|
|
168
187
|
});
|
|
169
188
|
// Post-task flow: push, PR, cleanup
|
|
170
189
|
await postTaskFlow({
|
package/dist/services/claude.js
CHANGED
|
@@ -85,19 +85,26 @@ export function configureDamperMcp() {
|
|
|
85
85
|
* Launch Claude Code in a directory
|
|
86
86
|
*/
|
|
87
87
|
export async function launchClaude(options) {
|
|
88
|
-
const { cwd, taskId, taskTitle, apiKey, yolo } = options;
|
|
89
|
-
|
|
88
|
+
const { cwd, taskId, taskTitle, apiKey, yolo, isResume } = options;
|
|
89
|
+
if (isResume) {
|
|
90
|
+
console.log(pc.green(`\nResuming Claude Code for task #${shortIdRaw(taskId)}: ${taskTitle}`));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log(pc.green(`\nStarting Claude Code for task #${shortIdRaw(taskId)}: ${taskTitle}`));
|
|
94
|
+
}
|
|
90
95
|
console.log(pc.dim(`Directory: ${cwd}`));
|
|
91
96
|
console.log();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
if (!isResume) {
|
|
98
|
+
// Show workflow explanation only for new tasks
|
|
99
|
+
console.log(pc.bold('Workflow:'));
|
|
100
|
+
console.log(pc.dim(' 1. Claude reads TASK_CONTEXT.md and creates a plan'));
|
|
101
|
+
if (!yolo) {
|
|
102
|
+
console.log(pc.dim(' 2. You approve the plan before Claude makes changes'));
|
|
103
|
+
}
|
|
104
|
+
console.log(pc.dim(` ${yolo ? '2' : '3'}. Claude implements the task, logging progress to Damper`));
|
|
105
|
+
console.log(pc.dim(` ${yolo ? '3' : '4'}. Claude calls complete_task (or abandon_task) when done`));
|
|
106
|
+
console.log();
|
|
107
|
+
}
|
|
101
108
|
console.log(pc.bold('When finished:'));
|
|
102
109
|
console.log(pc.dim(' • Push your branch and create a PR if needed'));
|
|
103
110
|
console.log(pc.dim(' • Run: npx @damper/cli cleanup'));
|
|
@@ -110,18 +117,29 @@ export async function launchClaude(options) {
|
|
|
110
117
|
console.log(pc.cyan('Mode: Plan (will ask for approval)'));
|
|
111
118
|
}
|
|
112
119
|
console.log();
|
|
113
|
-
// Build
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
// Build args based on whether we're resuming or starting fresh
|
|
121
|
+
let args;
|
|
122
|
+
if (isResume) {
|
|
123
|
+
// Resume previous conversation so Claude continues where it left off
|
|
124
|
+
const resumePrompt = `TASK_CONTEXT.md has been refreshed with the latest notes from Damper. Continue working on task #${taskId}: ${taskTitle}`;
|
|
125
|
+
args = yolo
|
|
126
|
+
? ['--continue', '--dangerously-skip-permissions', resumePrompt]
|
|
127
|
+
: ['--continue', resumePrompt];
|
|
128
|
+
console.log(pc.dim(`Resuming Claude session in ${cwd}...`));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// New task - enter plan mode first
|
|
132
|
+
const planInstruction = yolo
|
|
133
|
+
? ' After reading TASK_CONTEXT.md, use the EnterPlanMode tool to create an implementation plan, then execute it without waiting for user approval.'
|
|
134
|
+
: ' After reading TASK_CONTEXT.md, use the EnterPlanMode tool to create an implementation plan for user approval before making any code changes.';
|
|
135
|
+
const initialPrompt = `IMPORTANT: Start by reading TASK_CONTEXT.md completely. It contains the task description, implementation plan, critical rules, and architecture context.${planInstruction} Task #${taskId}: ${taskTitle}`;
|
|
136
|
+
args = yolo ? ['--dangerously-skip-permissions', initialPrompt] : [initialPrompt];
|
|
137
|
+
console.log(pc.dim(`Launching Claude in ${cwd}...`));
|
|
138
|
+
}
|
|
120
139
|
// Launch Claude Code
|
|
121
140
|
// Use spawn with stdio: 'inherit' for proper TTY passthrough
|
|
122
141
|
// Signals (Ctrl+C, Escape) are handled naturally since child inherits the terminal
|
|
123
142
|
await new Promise((resolve) => {
|
|
124
|
-
const args = yolo ? ['--dangerously-skip-permissions', initialPrompt] : [initialPrompt];
|
|
125
143
|
const child = spawn('claude', args, {
|
|
126
144
|
cwd,
|
|
127
145
|
stdio: 'inherit',
|
|
@@ -313,32 +331,41 @@ export async function postTaskFlow(options) {
|
|
|
313
331
|
}
|
|
314
332
|
}
|
|
315
333
|
else if (taskStatus === 'in_progress') {
|
|
316
|
-
// Task not completed -
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const error = err;
|
|
325
|
-
console.log(pc.yellow(`Could not release task: ${error.message}`));
|
|
326
|
-
}
|
|
327
|
-
// Ask about worktree cleanup
|
|
328
|
-
const shouldRemove = await confirm({
|
|
329
|
-
message: 'Remove worktree and branch?',
|
|
330
|
-
default: false,
|
|
334
|
+
// Task not completed - ask whether to keep lock for later resumption
|
|
335
|
+
const exitAction = await select({
|
|
336
|
+
message: 'Task is still in progress. What would you like to do?',
|
|
337
|
+
choices: [
|
|
338
|
+
{ name: 'Keep lock (resume later)', value: 'keep' },
|
|
339
|
+
{ name: 'Release lock (let others pick it up)', value: 'release' },
|
|
340
|
+
{ name: 'Release lock and remove worktree', value: 'release_cleanup' },
|
|
341
|
+
],
|
|
331
342
|
});
|
|
332
|
-
if (
|
|
343
|
+
if (exitAction === 'release' || exitAction === 'release_cleanup') {
|
|
344
|
+
console.log(pc.dim('\nReleasing task lock...'));
|
|
333
345
|
try {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
console.log(pc.green('✓
|
|
346
|
+
const api = createDamperApi(apiKey);
|
|
347
|
+
await api.abandonTask(taskId, 'Session ended via CLI');
|
|
348
|
+
console.log(pc.green('✓ Task released back to planned status'));
|
|
337
349
|
}
|
|
338
350
|
catch (err) {
|
|
339
351
|
const error = err;
|
|
340
|
-
console.log(pc.
|
|
352
|
+
console.log(pc.yellow(`Could not release task: ${error.message}`));
|
|
341
353
|
}
|
|
354
|
+
if (exitAction === 'release_cleanup') {
|
|
355
|
+
try {
|
|
356
|
+
await removeWorktreeDir(cwd, projectRoot);
|
|
357
|
+
worktreeRemoved = true;
|
|
358
|
+
console.log(pc.green('✓ Worktree and branch removed\n'));
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
const error = err;
|
|
362
|
+
console.log(pc.red(`Failed to remove worktree: ${error.message}\n`));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
console.log(pc.green('✓ Lock kept — resume with:'));
|
|
368
|
+
console.log(pc.cyan(` npx @damper/cli --task ${taskId}`));
|
|
342
369
|
}
|
|
343
370
|
}
|
|
344
371
|
// Offer to delete CLI-created tasks with no work done
|
package/dist/ui/format.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { Task } from '../services/damper-api.js';
|
|
2
|
+
/** Strip ANSI escape codes from a string */
|
|
3
|
+
export declare function stripAnsi(str: string): string;
|
|
4
|
+
/** ANSI-aware right-pad: pads to `width` based on visible (non-ANSI) length */
|
|
5
|
+
export declare function padEnd(str: string, width: number): string;
|
|
6
|
+
/** ANSI-aware left-pad: pads to `width` based on visible (non-ANSI) length */
|
|
7
|
+
export declare function padStart(str: string, width: number): string;
|
|
2
8
|
/** Colored short ID: dim `#` + cyan first 8 chars */
|
|
3
9
|
export declare function shortId(id: string): string;
|
|
4
10
|
/** Plain 8-char slice (for use inside other color wrappers) */
|
|
@@ -11,6 +17,13 @@ export declare function getPriorityIcon(priority: string): string;
|
|
|
11
17
|
export declare function formatEffort(effort: string | null | undefined): string;
|
|
12
18
|
/** Dim feedback count like `💬 3`, or empty string */
|
|
13
19
|
export declare function formatFeedback(count: number | null | undefined): string;
|
|
20
|
+
/** Compact progress fraction like `3/5`, or empty string */
|
|
21
|
+
export declare function formatProgressCompact(progress: {
|
|
22
|
+
done: number;
|
|
23
|
+
total: number;
|
|
24
|
+
} | null | undefined): string;
|
|
25
|
+
/** Due-date string with urgency coloring */
|
|
26
|
+
export declare function formatDueDate(dueDate: string | null | undefined): string;
|
|
14
27
|
/** Dim labels joined with ` · `, or empty string */
|
|
15
28
|
export declare function formatLabels(labels: string[] | null | undefined): string;
|
|
16
29
|
/** 10-char progress bar: green filled, dim empty, with count */
|
package/dist/ui/format.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
+
// ── ANSI-aware string utilities ──
|
|
3
|
+
/** Strip ANSI escape codes from a string */
|
|
4
|
+
export function stripAnsi(str) {
|
|
5
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
6
|
+
}
|
|
7
|
+
/** ANSI-aware right-pad: pads to `width` based on visible (non-ANSI) length */
|
|
8
|
+
export function padEnd(str, width) {
|
|
9
|
+
const visible = stripAnsi(str).length;
|
|
10
|
+
return visible >= width ? str : str + ' '.repeat(width - visible);
|
|
11
|
+
}
|
|
12
|
+
/** ANSI-aware left-pad: pads to `width` based on visible (non-ANSI) length */
|
|
13
|
+
export function padStart(str, width) {
|
|
14
|
+
const visible = stripAnsi(str).length;
|
|
15
|
+
return visible >= width ? str : ' '.repeat(width - visible) + str;
|
|
16
|
+
}
|
|
2
17
|
/** Colored short ID: dim `#` + cyan first 8 chars */
|
|
3
18
|
export function shortId(id) {
|
|
4
19
|
return `${pc.dim('#')}${pc.cyan(id.slice(0, 8))}`;
|
|
@@ -21,7 +36,7 @@ export function getPriorityIcon(priority) {
|
|
|
21
36
|
switch (priority) {
|
|
22
37
|
case 'high': return '\u{1F534} ';
|
|
23
38
|
case 'medium': return '\u{1F7E1} ';
|
|
24
|
-
default: return '';
|
|
39
|
+
default: return ' ';
|
|
25
40
|
}
|
|
26
41
|
}
|
|
27
42
|
/** Dim effort badge like `▪ L`, or empty string */
|
|
@@ -36,6 +51,42 @@ export function formatFeedback(count) {
|
|
|
36
51
|
return '';
|
|
37
52
|
return pc.dim(`\u{1F4AC} ${count}`);
|
|
38
53
|
}
|
|
54
|
+
/** Compact progress fraction like `3/5`, or empty string */
|
|
55
|
+
export function formatProgressCompact(progress) {
|
|
56
|
+
if (!progress || progress.total === 0)
|
|
57
|
+
return '';
|
|
58
|
+
return `${progress.done}/${progress.total}`;
|
|
59
|
+
}
|
|
60
|
+
const MONTH_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
61
|
+
/** Due-date string with urgency coloring */
|
|
62
|
+
export function formatDueDate(dueDate) {
|
|
63
|
+
if (!dueDate)
|
|
64
|
+
return '';
|
|
65
|
+
const due = new Date(dueDate);
|
|
66
|
+
if (isNaN(due.getTime()))
|
|
67
|
+
return '';
|
|
68
|
+
const now = new Date();
|
|
69
|
+
// Compare dates only (strip time)
|
|
70
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
71
|
+
const dueStart = new Date(due.getFullYear(), due.getMonth(), due.getDate());
|
|
72
|
+
const diffDays = Math.round((dueStart.getTime() - todayStart.getTime()) / (1000 * 60 * 60 * 24));
|
|
73
|
+
if (diffDays < -1)
|
|
74
|
+
return pc.red(`${Math.abs(diffDays)}d over`);
|
|
75
|
+
if (diffDays === -1)
|
|
76
|
+
return pc.red('1d over');
|
|
77
|
+
if (diffDays === 0)
|
|
78
|
+
return pc.red('today');
|
|
79
|
+
if (diffDays === 1)
|
|
80
|
+
return pc.red('tmrw');
|
|
81
|
+
if (diffDays <= 7)
|
|
82
|
+
return pc.yellow(`in ${diffDays}d`);
|
|
83
|
+
const month = MONTH_SHORT[due.getMonth()];
|
|
84
|
+
const day = due.getDate();
|
|
85
|
+
if (due.getFullYear() !== now.getFullYear()) {
|
|
86
|
+
return pc.dim(`${month} ${day} '${String(due.getFullYear()).slice(2)}`);
|
|
87
|
+
}
|
|
88
|
+
return pc.dim(`${month} ${day}`);
|
|
89
|
+
}
|
|
39
90
|
/** Dim labels joined with ` · `, or empty string */
|
|
40
91
|
export function formatLabels(labels) {
|
|
41
92
|
if (!labels || labels.length === 0)
|
package/dist/ui/task-picker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { select, confirm, input, Separator } from '@inquirer/prompts';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
|
-
import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort,
|
|
3
|
+
import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort, formatProgressCompact, formatDueDate, sectionHeader, relativeTime, getTerminalWidth, padEnd, padStart, } from './format.js';
|
|
4
4
|
// Layout constants (terminal column widths)
|
|
5
5
|
const CURSOR_WIDTH = 2; // inquirer select prefix (❯ or )
|
|
6
6
|
const PRIORITY_WIDTH = 3; // emoji(2) + space(1), or 3 spaces
|
|
@@ -9,18 +9,47 @@ const GAP1 = 1; // space after type
|
|
|
9
9
|
const ID_WIDTH = 9; // # + 8 chars
|
|
10
10
|
const GAP2 = 2; // spaces after ID
|
|
11
11
|
const GAP3 = 2; // spaces between title and meta
|
|
12
|
-
const META_RESERVE = 16; // max width for right-side meta
|
|
13
12
|
const MIN_TITLE = 20; // minimum title column width
|
|
13
|
+
// Column widths for meta area
|
|
14
|
+
const COL_EFFORT = 4; // "▪ L "
|
|
15
|
+
const COL_PROGRESS = 5; // " 3/5" (right-aligned)
|
|
16
|
+
const COL_FEEDBACK = 4; // "💬2 "
|
|
17
|
+
const COL_DUE_DATE = 8; // "in 3d " or " Mar 15"
|
|
18
|
+
const COL_GAP = 2; // gap between meta columns
|
|
19
|
+
function getMetaLayout(tasks, termWidth) {
|
|
20
|
+
const hasAnyDueDate = tasks.some(t => t.dueDate);
|
|
21
|
+
const hasAnyProgress = tasks.some(t => t.subtaskProgress?.total);
|
|
22
|
+
const hasAnyFeedback = tasks.some(t => t.feedbackCount);
|
|
23
|
+
const narrow = termWidth < 100;
|
|
24
|
+
const effort = true; // always show effort column
|
|
25
|
+
const dueDate = hasAnyDueDate;
|
|
26
|
+
const progress = !narrow && hasAnyProgress;
|
|
27
|
+
const feedback = !narrow && hasAnyFeedback;
|
|
28
|
+
// Calculate total width: sum of active columns + gaps between them
|
|
29
|
+
const activeCols = [];
|
|
30
|
+
if (effort)
|
|
31
|
+
activeCols.push(COL_EFFORT);
|
|
32
|
+
if (progress)
|
|
33
|
+
activeCols.push(COL_PROGRESS);
|
|
34
|
+
if (feedback)
|
|
35
|
+
activeCols.push(COL_FEEDBACK);
|
|
36
|
+
if (dueDate)
|
|
37
|
+
activeCols.push(COL_DUE_DATE);
|
|
38
|
+
const totalWidth = activeCols.length > 0
|
|
39
|
+
? activeCols.reduce((a, b) => a + b, 0) + (activeCols.length - 1) * COL_GAP
|
|
40
|
+
: 0;
|
|
41
|
+
return { effort, progress, feedback, dueDate, totalWidth };
|
|
42
|
+
}
|
|
14
43
|
const LEFT_FIXED = CURSOR_WIDTH + PRIORITY_WIDTH + TYPE_WIDTH + GAP1 + ID_WIDTH + GAP2; // 19
|
|
15
44
|
const INDENT_WIDTH = PRIORITY_WIDTH + TYPE_WIDTH + GAP1 + ID_WIDTH + GAP2; // 17 (without cursor)
|
|
16
|
-
function getTitleWidth() {
|
|
45
|
+
function getTitleWidth(layout) {
|
|
17
46
|
const termWidth = getTerminalWidth();
|
|
18
|
-
|
|
47
|
+
const metaWidth = layout.totalWidth > 0 ? GAP3 + layout.totalWidth : 0;
|
|
48
|
+
return Math.max(MIN_TITLE, termWidth - LEFT_FIXED - metaWidth);
|
|
19
49
|
}
|
|
20
|
-
function formatTableRow(task, titleWidth) {
|
|
21
|
-
// Priority: always 3 terminal cols
|
|
22
|
-
const
|
|
23
|
-
const priorityStr = hasPriority ? getPriorityIcon(task.priority) : ' ';
|
|
50
|
+
function formatTableRow(task, titleWidth, layout) {
|
|
51
|
+
// Priority: always 3 terminal cols (getPriorityIcon returns 3 spaces for non-priority)
|
|
52
|
+
const priorityStr = getPriorityIcon(task.priority);
|
|
24
53
|
// Type icon: 2 terminal cols
|
|
25
54
|
const typeStr = getTypeIcon(task.type);
|
|
26
55
|
// ID: 9 visible chars
|
|
@@ -29,19 +58,33 @@ function formatTableRow(task, titleWidth) {
|
|
|
29
58
|
const titleStr = task.title.length > titleWidth
|
|
30
59
|
? task.title.slice(0, titleWidth - 1) + '\u2026'
|
|
31
60
|
: task.title + ' '.repeat(titleWidth - task.title.length);
|
|
32
|
-
//
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
// Build fixed-width meta columns
|
|
62
|
+
const metaCols = [];
|
|
63
|
+
if (layout.effort) {
|
|
64
|
+
const effortStr = task.effort ? formatEffort(task.effort) : '';
|
|
65
|
+
metaCols.push(padEnd(effortStr, COL_EFFORT));
|
|
66
|
+
}
|
|
67
|
+
if (layout.progress) {
|
|
68
|
+
const progressStr = task.subtaskProgress?.total
|
|
69
|
+
? formatProgressCompact(task.subtaskProgress)
|
|
70
|
+
: '';
|
|
71
|
+
metaCols.push(padStart(progressStr, COL_PROGRESS));
|
|
72
|
+
}
|
|
73
|
+
if (layout.feedback) {
|
|
74
|
+
const feedbackStr = task.feedbackCount
|
|
75
|
+
? pc.dim(`\u{1F4AC}${task.feedbackCount}`)
|
|
76
|
+
: '';
|
|
77
|
+
metaCols.push(padEnd(feedbackStr, COL_FEEDBACK));
|
|
78
|
+
}
|
|
79
|
+
if (layout.dueDate) {
|
|
80
|
+
const dueDateStr = formatDueDate(task.dueDate);
|
|
81
|
+
metaCols.push(padStart(dueDateStr, COL_DUE_DATE));
|
|
82
|
+
}
|
|
83
|
+
const meta = metaCols.length > 0 ? ' ' + metaCols.join(' '.repeat(COL_GAP)) : '';
|
|
84
|
+
return `${priorityStr}${typeStr} ${idStr} ${titleStr}${meta}`;
|
|
42
85
|
}
|
|
43
|
-
function formatTaskChoice(choice, titleWidth) {
|
|
44
|
-
const firstLine = formatTableRow(choice.task, titleWidth);
|
|
86
|
+
function formatTaskChoice(choice, titleWidth, layout) {
|
|
87
|
+
const firstLine = formatTableRow(choice.task, titleWidth, layout);
|
|
45
88
|
const indent = ' '.repeat(INDENT_WIDTH);
|
|
46
89
|
if (choice.type === 'in_progress') {
|
|
47
90
|
const worktreeName = choice.worktree.path.split('/').pop() || choice.worktree.path;
|
|
@@ -75,7 +118,6 @@ function formatTaskChoice(choice, titleWidth) {
|
|
|
75
118
|
}
|
|
76
119
|
export async function pickTask(options) {
|
|
77
120
|
const { api, worktrees, typeFilter, statusFilter } = options;
|
|
78
|
-
const titleWidth = getTitleWidth();
|
|
79
121
|
// Fetch tasks from Damper
|
|
80
122
|
const { tasks, project } = await api.listTasks({
|
|
81
123
|
status: statusFilter || 'all',
|
|
@@ -118,12 +160,20 @@ export async function pickTask(options) {
|
|
|
118
160
|
availableChoices.push({ type: 'available', task });
|
|
119
161
|
}
|
|
120
162
|
}
|
|
163
|
+
// Compute adaptive column layout based on terminal width and task data
|
|
164
|
+
const allDisplayTasks = [
|
|
165
|
+
...inProgressChoices.map(c => c.task),
|
|
166
|
+
...availableChoices.map(c => c.task),
|
|
167
|
+
...lockedChoices.map(c => c.task),
|
|
168
|
+
];
|
|
169
|
+
const layout = getMetaLayout(allDisplayTasks, getTerminalWidth());
|
|
170
|
+
const titleWidth = getTitleWidth(layout);
|
|
121
171
|
const choices = [];
|
|
122
172
|
if (inProgressChoices.length > 0) {
|
|
123
173
|
choices.push(new Separator(`\n${sectionHeader(`In Progress (${inProgressChoices.length})`)}`));
|
|
124
174
|
for (const choice of inProgressChoices) {
|
|
125
175
|
choices.push({
|
|
126
|
-
name: formatTaskChoice(choice, titleWidth),
|
|
176
|
+
name: formatTaskChoice(choice, titleWidth, layout),
|
|
127
177
|
value: choice,
|
|
128
178
|
});
|
|
129
179
|
}
|
|
@@ -132,7 +182,7 @@ export async function pickTask(options) {
|
|
|
132
182
|
choices.push(new Separator(`\n${sectionHeader(`Available (${availableChoices.length})`)}`));
|
|
133
183
|
for (const choice of availableChoices) {
|
|
134
184
|
choices.push({
|
|
135
|
-
name: formatTaskChoice(choice, titleWidth),
|
|
185
|
+
name: formatTaskChoice(choice, titleWidth, layout),
|
|
136
186
|
value: choice,
|
|
137
187
|
});
|
|
138
188
|
}
|
|
@@ -141,7 +191,7 @@ export async function pickTask(options) {
|
|
|
141
191
|
choices.push(new Separator(`\n${sectionHeader(`Locked (${lockedChoices.length})`)}`));
|
|
142
192
|
for (const choice of lockedChoices) {
|
|
143
193
|
choices.push({
|
|
144
|
-
name: formatTaskChoice(choice, titleWidth),
|
|
194
|
+
name: formatTaskChoice(choice, titleWidth, layout),
|
|
145
195
|
value: choice,
|
|
146
196
|
});
|
|
147
197
|
}
|