@damper/cli 0.7.2 → 0.9.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.
@@ -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({
@@ -31,6 +31,7 @@ export declare function launchClaude(options: {
31
31
  taskTitle: string;
32
32
  apiKey: string;
33
33
  yolo?: boolean;
34
+ isResume?: boolean;
34
35
  }): Promise<{
35
36
  cwd: string;
36
37
  taskId: string;
@@ -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
- console.log(pc.green(`\nStarting Claude Code for task #${shortIdRaw(taskId)}: ${taskTitle}`));
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
- // Show workflow explanation
93
- console.log(pc.bold('Workflow:'));
94
- console.log(pc.dim(' 1. Claude reads TASK_CONTEXT.md and creates a plan'));
95
- if (!yolo) {
96
- console.log(pc.dim(' 2. You approve the plan before Claude makes changes'));
97
- }
98
- console.log(pc.dim(` ${yolo ? '2' : '3'}. Claude implements the task, logging progress to Damper`));
99
- console.log(pc.dim(` ${yolo ? '3' : '4'}. Claude calls complete_task (or abandon_task) when done`));
100
- console.log();
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 prompt - context is in TASK_CONTEXT.md, MCP is in .claude/settings.json
114
- // Always enter plan mode first. In yolo mode, execute without waiting for approval.
115
- const planInstruction = yolo
116
- ? ' After reading TASK_CONTEXT.md, use the EnterPlanMode tool to create an implementation plan, then execute it without waiting for user approval.'
117
- : ' After reading TASK_CONTEXT.md, use the EnterPlanMode tool to create an implementation plan for user approval before making any code changes.';
118
- 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}`;
119
- console.log(pc.dim(`Launching Claude in ${cwd}...`));
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',
@@ -256,17 +274,41 @@ export async function postTaskFlow(options) {
256
274
  console.log(pc.dim('Keeping changes local.\n'));
257
275
  }
258
276
  else if (mergeAction === 'merge') {
259
- // Merge directly to main
277
+ // Merge directly to main — first merge main into feature to resolve conflicts
260
278
  try {
261
279
  const { execa } = await import('execa');
262
- console.log(pc.dim('Switching to main and merging...'));
263
280
  // Fetch latest main
264
281
  await execa('git', ['fetch', 'origin', 'main'], { cwd, stdio: 'pipe' });
265
- // Checkout main in the main project root (not worktree)
266
- await execa('git', ['checkout', 'main'], { cwd: projectRoot, stdio: 'pipe' });
267
- // Merge the feature branch
268
- await execa('git', ['merge', currentBranch, '--no-edit'], { cwd: projectRoot, stdio: 'inherit' });
269
- console.log(pc.green(' Merged to main locally\n'));
282
+ // Step 1: Merge origin/main INTO the feature branch (in worktree)
283
+ console.log(pc.dim('Merging main into feature branch...'));
284
+ let mainMergedIntoFeature = false;
285
+ try {
286
+ await execa('git', ['merge', 'origin/main', '--no-edit'], { cwd, stdio: 'pipe' });
287
+ mainMergedIntoFeature = true;
288
+ console.log(pc.green('✓ Main merged into feature branch (no conflicts)'));
289
+ }
290
+ catch {
291
+ // Merge conflicts — launch Claude to resolve
292
+ console.log(pc.yellow('Merge conflicts detected. Launching Claude to resolve...'));
293
+ await execa('git', ['merge', '--abort'], { cwd, stdio: 'pipe' });
294
+ await launchClaudeForMerge({ cwd, apiKey });
295
+ // Verify merge was completed (origin/main should be ancestor of HEAD)
296
+ try {
297
+ await execa('git', ['merge-base', '--is-ancestor', 'origin/main', 'HEAD'], { cwd, stdio: 'pipe' });
298
+ mainMergedIntoFeature = true;
299
+ console.log(pc.green('✓ Claude resolved merge conflicts'));
300
+ }
301
+ catch {
302
+ console.log(pc.red('Claude did not complete the merge. Skipping merge to main.\n'));
303
+ }
304
+ }
305
+ // Step 2: Merge feature → main (should be clean now)
306
+ if (mainMergedIntoFeature) {
307
+ console.log(pc.dim('Merging feature into main...'));
308
+ await execa('git', ['checkout', 'main'], { cwd: projectRoot, stdio: 'pipe' });
309
+ await execa('git', ['merge', currentBranch, '--no-edit'], { cwd: projectRoot, stdio: 'inherit' });
310
+ console.log(pc.green('✓ Merged to main locally\n'));
311
+ }
270
312
  }
271
313
  catch (err) {
272
314
  console.log(pc.red('Failed to merge. You may need to resolve conflicts manually.\n'));
@@ -313,33 +355,42 @@ export async function postTaskFlow(options) {
313
355
  }
314
356
  }
315
357
  else if (taskStatus === 'in_progress') {
316
- // Task not completed - always release the lock, optionally remove worktree
317
- console.log(pc.dim('\nReleasing task lock...'));
318
- try {
319
- const api = createDamperApi(apiKey);
320
- await api.abandonTask(taskId, 'Session ended via CLI');
321
- console.log(pc.green(`✓ Task released back to planned status`));
322
- }
323
- catch (err) {
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,
358
+ // Task not completed - ask whether to keep lock for later resumption
359
+ const exitAction = await select({
360
+ message: 'Task is still in progress. What would you like to do?',
361
+ choices: [
362
+ { name: 'Keep lock (resume later)', value: 'keep' },
363
+ { name: 'Release lock (let others pick it up)', value: 'release' },
364
+ { name: 'Release lock and remove worktree', value: 'release_cleanup' },
365
+ ],
331
366
  });
332
- if (shouldRemove) {
367
+ if (exitAction === 'release' || exitAction === 'release_cleanup') {
368
+ console.log(pc.dim('\nReleasing task lock...'));
333
369
  try {
334
- await removeWorktreeDir(cwd, projectRoot);
335
- worktreeRemoved = true;
336
- console.log(pc.green('✓ Worktree and branch removed\n'));
370
+ const api = createDamperApi(apiKey);
371
+ await api.abandonTask(taskId, 'Session ended via CLI');
372
+ console.log(pc.green('✓ Task released back to planned status'));
337
373
  }
338
374
  catch (err) {
339
375
  const error = err;
340
- console.log(pc.red(`Failed to remove worktree: ${error.message}\n`));
376
+ console.log(pc.yellow(`Could not release task: ${error.message}`));
377
+ }
378
+ if (exitAction === 'release_cleanup') {
379
+ try {
380
+ await removeWorktreeDir(cwd, projectRoot);
381
+ worktreeRemoved = true;
382
+ console.log(pc.green('✓ Worktree and branch removed\n'));
383
+ }
384
+ catch (err) {
385
+ const error = err;
386
+ console.log(pc.red(`Failed to remove worktree: ${error.message}\n`));
387
+ }
341
388
  }
342
389
  }
390
+ else {
391
+ console.log(pc.green('✓ Lock kept — resume with:'));
392
+ console.log(pc.cyan(` npx @damper/cli --task ${taskId}`));
393
+ }
343
394
  }
344
395
  // Offer to delete CLI-created tasks with no work done
345
396
  let taskDeleted = false;
@@ -379,6 +430,22 @@ export async function postTaskFlow(options) {
379
430
  }
380
431
  console.log();
381
432
  }
433
+ /**
434
+ * Launch Claude to resolve merge conflicts
435
+ */
436
+ async function launchClaudeForMerge(options) {
437
+ const { cwd, apiKey } = options;
438
+ const prompt = 'Merge origin/main into the current branch and resolve any conflicts. Run: git merge origin/main --no-edit. If there are conflicts, resolve them, then stage and commit.';
439
+ await new Promise((resolve) => {
440
+ const child = spawn('claude', [prompt], {
441
+ cwd,
442
+ stdio: 'inherit',
443
+ env: { ...process.env, DAMPER_API_KEY: apiKey },
444
+ });
445
+ child.on('error', () => resolve());
446
+ child.on('close', () => resolve());
447
+ });
448
+ }
382
449
  /**
383
450
  * Check if Claude Code CLI is installed
384
451
  */
@@ -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)
@@ -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, formatSubtaskProgress, sectionHeader, relativeTime, getTerminalWidth, } from './format.js';
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
- return Math.max(MIN_TITLE, termWidth - LEFT_FIXED - GAP3 - META_RESERVE);
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 hasPriority = task.priority === 'high' || task.priority === 'medium';
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
- // Right-side meta (compact)
33
- const metaParts = [];
34
- if (task.effort)
35
- metaParts.push(formatEffort(task.effort));
36
- if (task.feedbackCount)
37
- metaParts.push(pc.dim(`\u{1F4AC}${task.feedbackCount}`));
38
- if (task.subtaskProgress?.total)
39
- metaParts.push(formatSubtaskProgress(task.subtaskProgress));
40
- const meta = metaParts.join(' ');
41
- return `${priorityStr}${typeStr} ${idStr} ${titleStr} ${meta}`;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damper/cli",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
4
4
  "description": "CLI tool for orchestrating Damper task workflows with Claude Code",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {