@damper/cli 0.7.1 → 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.
@@ -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',
@@ -313,32 +331,41 @@ export async function postTaskFlow(options) {
313
331
  }
314
332
  }
315
333
  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,
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 (shouldRemove) {
343
+ if (exitAction === 'release' || exitAction === 'release_cleanup') {
344
+ console.log(pc.dim('\nReleasing task lock...'));
333
345
  try {
334
- await removeWorktreeDir(cwd, projectRoot);
335
- worktreeRemoved = true;
336
- console.log(pc.green('✓ Worktree and branch removed\n'));
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.red(`Failed to remove worktree: ${error.message}\n`));
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
@@ -268,20 +268,29 @@ export async function createWorktree(options) {
268
268
  };
269
269
  fs.writeFileSync(path.join(worktreePath, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
270
270
  console.log(pc.dim('Created .mcp.json with Damper MCP'));
271
- // Create .claude/settings.local.json with permissions for Damper MCP tools
271
+ // Create .claude/settings.local.json - copy root permissions and ensure Damper MCP is allowed
272
272
  const claudeDir = path.join(worktreePath, '.claude');
273
273
  if (!fs.existsSync(claudeDir)) {
274
274
  fs.mkdirSync(claudeDir, { recursive: true });
275
275
  }
276
- const claudeSettings = {
277
- permissions: {
278
- allow: [
279
- 'mcp__damper__*',
280
- ],
281
- },
282
- };
276
+ // Read existing settings from root project
277
+ const rootSettingsPath = path.join(projectRoot, '.claude', 'settings.local.json');
278
+ let claudeSettings = {};
279
+ if (fs.existsSync(rootSettingsPath)) {
280
+ try {
281
+ claudeSettings = JSON.parse(fs.readFileSync(rootSettingsPath, 'utf-8'));
282
+ }
283
+ catch { }
284
+ }
285
+ // Ensure permissions.allow includes damper MCP
286
+ const permissions = (claudeSettings.permissions ?? {});
287
+ const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
288
+ if (!allow.includes('mcp__damper__*')) {
289
+ allow.unshift('mcp__damper__*');
290
+ }
291
+ claudeSettings.permissions = { ...permissions, allow };
283
292
  fs.writeFileSync(path.join(claudeDir, 'settings.local.json'), JSON.stringify(claudeSettings, null, 2));
284
- console.log(pc.dim('Created .claude/settings.local.json with MCP permissions'));
293
+ console.log(pc.dim('Created .claude/settings.local.json with root permissions + MCP'));
285
294
  // Save to state
286
295
  const worktreeState = {
287
296
  taskId,
@@ -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.1",
3
+ "version": "0.8.0",
4
4
  "description": "CLI tool for orchestrating Damper task workflows with Claude Code",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {