@damper/cli 0.6.10 → 0.6.12
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/cleanup.js +3 -5
- package/dist/commands/release.js +8 -3
- package/dist/commands/start.js +4 -3
- package/dist/commands/status.js +31 -11
- package/dist/services/claude.js +2 -1
- package/dist/ui/format.d.ts +30 -0
- package/dist/ui/format.js +139 -0
- package/dist/ui/task-picker.js +21 -38
- package/package.json +1 -1
package/dist/commands/cleanup.js
CHANGED
|
@@ -5,6 +5,7 @@ import pc from 'picocolors';
|
|
|
5
5
|
import { getWorktrees, cleanupStaleWorktrees, removeWorktree } from '../services/state.js';
|
|
6
6
|
import { createDamperApi } from '../services/damper-api.js';
|
|
7
7
|
import { removeWorktreeDir } from '../services/worktree.js';
|
|
8
|
+
import { shortId, shortIdRaw, formatTaskLine } from '../ui/format.js';
|
|
8
9
|
async function hasUncommittedChanges(worktreePath) {
|
|
9
10
|
try {
|
|
10
11
|
const { stdout } = await execa('git', ['status', '--porcelain'], {
|
|
@@ -108,10 +109,7 @@ export async function cleanupCommand() {
|
|
|
108
109
|
console.log(pc.bold('\nWorktrees available for cleanup:\n'));
|
|
109
110
|
const choices = candidates.map(c => {
|
|
110
111
|
const worktreeName = c.worktree.path.split('/').pop() || c.worktree.path;
|
|
111
|
-
let description =
|
|
112
|
-
if (c.task) {
|
|
113
|
-
description += ` - ${c.task.title}`;
|
|
114
|
-
}
|
|
112
|
+
let description = c.task ? formatTaskLine(c.task) : shortId(c.worktree.taskId);
|
|
115
113
|
let reasonBadge = '';
|
|
116
114
|
switch (c.reason) {
|
|
117
115
|
case 'completed':
|
|
@@ -177,7 +175,7 @@ export async function cleanupCommand() {
|
|
|
177
175
|
if (candidate.reason === 'in_progress' && api) {
|
|
178
176
|
try {
|
|
179
177
|
await api.abandonTask(worktree.taskId, 'Dropped via CLI cleanup');
|
|
180
|
-
console.log(pc.green(`✓ Released task #${worktree.taskId}`));
|
|
178
|
+
console.log(pc.green(`✓ Released task #${shortIdRaw(worktree.taskId)}`));
|
|
181
179
|
}
|
|
182
180
|
catch (err) {
|
|
183
181
|
const error = err;
|
package/dist/commands/release.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createDamperApi } from '../services/damper-api.js';
|
|
|
4
4
|
import { getWorktrees, removeWorktree } from '../services/state.js';
|
|
5
5
|
import { removeWorktreeDir, getGitRoot } from '../services/worktree.js';
|
|
6
6
|
import { getApiKey } from '../services/config.js';
|
|
7
|
+
import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort } from '../ui/format.js';
|
|
7
8
|
export async function releaseCommand() {
|
|
8
9
|
// Get project root
|
|
9
10
|
let projectRoot;
|
|
@@ -34,10 +35,14 @@ export async function releaseCommand() {
|
|
|
34
35
|
const worktrees = getWorktrees();
|
|
35
36
|
const choices = tasks.map(task => {
|
|
36
37
|
const worktree = worktrees.find(w => w.taskId === task.id);
|
|
38
|
+
const priorityIcon = getPriorityIcon(task.priority);
|
|
39
|
+
const typeIcon = getTypeIcon(task.type);
|
|
40
|
+
const id = shortId(task.id);
|
|
41
|
+
const effort = formatEffort(task.effort);
|
|
37
42
|
const lockedInfo = task.lockedBy ? pc.dim(` (locked by ${task.lockedBy})`) : '';
|
|
38
43
|
const worktreeInfo = worktree ? pc.dim(` [has worktree]`) : '';
|
|
39
44
|
return {
|
|
40
|
-
name:
|
|
45
|
+
name: `${priorityIcon}${typeIcon} ${id} ${task.title}${effort ? ` ${effort}` : ''}${lockedInfo}${worktreeInfo}`,
|
|
41
46
|
value: { task, worktree },
|
|
42
47
|
};
|
|
43
48
|
});
|
|
@@ -49,7 +54,7 @@ export async function releaseCommand() {
|
|
|
49
54
|
// Confirm
|
|
50
55
|
console.log();
|
|
51
56
|
console.log(pc.yellow(`This will:`));
|
|
52
|
-
console.log(pc.dim(` • Release the lock on task #${task.id}`));
|
|
57
|
+
console.log(pc.dim(` • Release the lock on task #${shortIdRaw(task.id)}`));
|
|
53
58
|
console.log(pc.dim(` • Set task status back to "planned"`));
|
|
54
59
|
if (worktree) {
|
|
55
60
|
console.log(pc.dim(` • Optionally remove the worktree`));
|
|
@@ -66,7 +71,7 @@ export async function releaseCommand() {
|
|
|
66
71
|
// Abandon the task in Damper
|
|
67
72
|
try {
|
|
68
73
|
await api.abandonTask(task.id, 'Released via CLI');
|
|
69
|
-
console.log(pc.green(`\n✓ Task #${task.id} released to planned status`));
|
|
74
|
+
console.log(pc.green(`\n✓ Task #${shortIdRaw(task.id)} released to planned status`));
|
|
70
75
|
}
|
|
71
76
|
catch (err) {
|
|
72
77
|
const error = err;
|
package/dist/commands/start.js
CHANGED
|
@@ -7,6 +7,7 @@ import { pickTask } from '../ui/task-picker.js';
|
|
|
7
7
|
import { launchClaude, postTaskFlow, isClaudeInstalled, isDamperMcpConfigured, configureDamperMcp } from '../services/claude.js';
|
|
8
8
|
import { getWorktreesForProject, cleanupStaleWorktrees } from '../services/state.js';
|
|
9
9
|
import { getApiKey, isProjectConfigured, getProjectConfigPath } from '../services/config.js';
|
|
10
|
+
import { shortIdRaw } from '../ui/format.js';
|
|
10
11
|
export async function startCommand(options) {
|
|
11
12
|
// Check Claude CLI first
|
|
12
13
|
const claudeInstalled = await isClaudeInstalled();
|
|
@@ -68,10 +69,10 @@ export async function startCommand(options) {
|
|
|
68
69
|
if (existingWorktree && fs.existsSync(existingWorktree.path)) {
|
|
69
70
|
isResume = true;
|
|
70
71
|
worktreePath = existingWorktree.path;
|
|
71
|
-
console.log(pc.cyan(`\nResuming task #${taskId}: ${taskTitle}`));
|
|
72
|
+
console.log(pc.cyan(`\nResuming task #${shortIdRaw(taskId)}: ${taskTitle}`));
|
|
72
73
|
}
|
|
73
74
|
else {
|
|
74
|
-
console.log(pc.cyan(`\nStarting task #${taskId}: ${taskTitle}`));
|
|
75
|
+
console.log(pc.cyan(`\nStarting task #${shortIdRaw(taskId)}: ${taskTitle}`));
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
else {
|
|
@@ -95,7 +96,7 @@ export async function startCommand(options) {
|
|
|
95
96
|
}
|
|
96
97
|
if (isResume && worktreePath) {
|
|
97
98
|
// Resume existing worktree
|
|
98
|
-
console.log(pc.green(`\n✓ Resuming: #${taskId} ${taskTitle}`));
|
|
99
|
+
console.log(pc.green(`\n✓ Resuming: #${shortIdRaw(taskId)} ${taskTitle}`));
|
|
99
100
|
console.log(pc.dim(` Worktree: ${worktreePath}`));
|
|
100
101
|
// Refresh context with latest from Damper
|
|
101
102
|
console.log(pc.dim('\nRefreshing context from Damper...'));
|
package/dist/commands/status.js
CHANGED
|
@@ -3,6 +3,7 @@ import pc from 'picocolors';
|
|
|
3
3
|
import { getWorktrees, cleanupStaleWorktrees } from '../services/state.js';
|
|
4
4
|
import { createDamperApi } from '../services/damper-api.js';
|
|
5
5
|
import { getGitRoot } from '../services/worktree.js';
|
|
6
|
+
import { shortId, getTypeIcon, getPriorityIcon, formatEffort, formatSubtaskProgress, relativeTime, statusColor as getStatusColor, } from '../ui/format.js';
|
|
6
7
|
export async function statusCommand() {
|
|
7
8
|
// Clean up stale entries first
|
|
8
9
|
const stale = cleanupStaleWorktrees();
|
|
@@ -49,24 +50,43 @@ export async function statusCommand() {
|
|
|
49
50
|
console.log();
|
|
50
51
|
for (const wt of projectWorktrees) {
|
|
51
52
|
const exists = fs.existsSync(wt.path);
|
|
52
|
-
const
|
|
53
|
-
// Try to get task
|
|
54
|
-
let taskStatus = '';
|
|
53
|
+
const statusIcon = exists ? pc.green('●') : pc.red('○');
|
|
54
|
+
// Try to get task details from Damper
|
|
55
55
|
if (api) {
|
|
56
56
|
try {
|
|
57
57
|
const task = await api.getTask(wt.taskId);
|
|
58
|
-
const
|
|
59
|
-
|
|
58
|
+
const priorityIcon = getPriorityIcon(task.priority);
|
|
59
|
+
const typeIcon = getTypeIcon(task.type);
|
|
60
|
+
const colorFn = getStatusColor(task.status);
|
|
61
|
+
const statusBadge = colorFn(`[${task.status}]`);
|
|
62
|
+
const effort = formatEffort(task.effort);
|
|
63
|
+
const progress = formatSubtaskProgress(task.subtaskProgress);
|
|
64
|
+
const time = relativeTime(wt.createdAt);
|
|
65
|
+
console.log(` ${statusIcon} ${priorityIcon}${typeIcon} ${shortId(task.id)} ${task.title} ${statusBadge}${effort ? ` ${effort}` : ''}`);
|
|
66
|
+
const metaParts = [wt.branch];
|
|
67
|
+
if (time)
|
|
68
|
+
metaParts.push(time);
|
|
69
|
+
if (progress)
|
|
70
|
+
metaParts.push(progress);
|
|
71
|
+
console.log(` ${metaParts.join(` ${pc.dim('\u00B7')} `)}`);
|
|
60
72
|
}
|
|
61
73
|
catch {
|
|
62
|
-
|
|
74
|
+
console.log(` ${statusIcon} ${shortId(wt.taskId)} ${pc.dim('[unknown]')}`);
|
|
75
|
+
const time = relativeTime(wt.createdAt);
|
|
76
|
+
const metaParts = [wt.branch];
|
|
77
|
+
if (time)
|
|
78
|
+
metaParts.push(time);
|
|
79
|
+
console.log(` ${metaParts.join(` ${pc.dim('\u00B7')} `)}`);
|
|
63
80
|
}
|
|
64
81
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
82
|
+
else {
|
|
83
|
+
console.log(` ${statusIcon} ${shortId(wt.taskId)}`);
|
|
84
|
+
const time = relativeTime(wt.createdAt);
|
|
85
|
+
const metaParts = [wt.branch];
|
|
86
|
+
if (time)
|
|
87
|
+
metaParts.push(time);
|
|
88
|
+
console.log(` ${metaParts.join(` ${pc.dim('\u00B7')} `)}`);
|
|
89
|
+
}
|
|
70
90
|
console.log();
|
|
71
91
|
}
|
|
72
92
|
}
|
package/dist/services/claude.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as os from 'node:os';
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { execa } from 'execa';
|
|
6
6
|
import pc from 'picocolors';
|
|
7
|
+
import { shortIdRaw } from '../ui/format.js';
|
|
7
8
|
const CLAUDE_SETTINGS_DIR = path.join(os.homedir(), '.claude');
|
|
8
9
|
const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_SETTINGS_DIR, 'settings.json');
|
|
9
10
|
/**
|
|
@@ -85,7 +86,7 @@ export function configureDamperMcp() {
|
|
|
85
86
|
*/
|
|
86
87
|
export async function launchClaude(options) {
|
|
87
88
|
const { cwd, taskId, taskTitle, apiKey, yolo } = options;
|
|
88
|
-
console.log(pc.green(`\nStarting Claude Code for task #${taskId}: ${taskTitle}`));
|
|
89
|
+
console.log(pc.green(`\nStarting Claude Code for task #${shortIdRaw(taskId)}: ${taskTitle}`));
|
|
89
90
|
console.log(pc.dim(`Directory: ${cwd}`));
|
|
90
91
|
console.log();
|
|
91
92
|
// Show workflow explanation
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Task } from '../services/damper-api.js';
|
|
2
|
+
/** Colored short ID: dim `#` + cyan first 8 chars */
|
|
3
|
+
export declare function shortId(id: string): string;
|
|
4
|
+
/** Plain 8-char slice (for use inside other color wrappers) */
|
|
5
|
+
export declare function shortIdRaw(id: string): string;
|
|
6
|
+
/** Type emoji icon */
|
|
7
|
+
export declare function getTypeIcon(type: string): string;
|
|
8
|
+
/** Priority prefix: red/yellow circle + space, or empty */
|
|
9
|
+
export declare function getPriorityIcon(priority: string): string;
|
|
10
|
+
/** Dim effort badge like `▪ L`, or empty string */
|
|
11
|
+
export declare function formatEffort(effort: string | null | undefined): string;
|
|
12
|
+
/** Dim feedback count like `💬 3`, or empty string */
|
|
13
|
+
export declare function formatFeedback(count: number | null | undefined): string;
|
|
14
|
+
/** Dim labels joined with ` · `, or empty string */
|
|
15
|
+
export declare function formatLabels(labels: string[] | null | undefined): string;
|
|
16
|
+
/** 10-char progress bar: green filled, dim empty, with count */
|
|
17
|
+
export declare function formatSubtaskProgress(progress: {
|
|
18
|
+
done: number;
|
|
19
|
+
total: number;
|
|
20
|
+
} | null | undefined): string;
|
|
21
|
+
/** Relative time string from date, no dependencies */
|
|
22
|
+
export declare function relativeTime(date: string | Date | null | undefined): string;
|
|
23
|
+
/** Returns a picocolors function for the given status */
|
|
24
|
+
export declare function statusColor(status: string): (s: string) => string;
|
|
25
|
+
/** Box-drawing section header: `┌─ Label ───────────────┐` */
|
|
26
|
+
export declare function sectionHeader(label: string, width?: number): string;
|
|
27
|
+
/** Compact one-liner: `🔴 ✨ #a1b2c3d4 Title ▪ L` */
|
|
28
|
+
export declare function formatTaskLine(task: Task): string;
|
|
29
|
+
/** Dim metadata second line with segments joined by ` · ` */
|
|
30
|
+
export declare function formatTaskMeta(task: Task): string;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
/** Colored short ID: dim `#` + cyan first 8 chars */
|
|
3
|
+
export function shortId(id) {
|
|
4
|
+
return `${pc.dim('#')}${pc.cyan(id.slice(0, 8))}`;
|
|
5
|
+
}
|
|
6
|
+
/** Plain 8-char slice (for use inside other color wrappers) */
|
|
7
|
+
export function shortIdRaw(id) {
|
|
8
|
+
return id.slice(0, 8);
|
|
9
|
+
}
|
|
10
|
+
/** Type emoji icon */
|
|
11
|
+
export function getTypeIcon(type) {
|
|
12
|
+
switch (type) {
|
|
13
|
+
case 'bug': return '\u{1F41B}';
|
|
14
|
+
case 'feature': return '\u{2728}';
|
|
15
|
+
case 'improvement': return '\u{1F4A1}';
|
|
16
|
+
default: return '\u{1F4CC}';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Priority prefix: red/yellow circle + space, or empty */
|
|
20
|
+
export function getPriorityIcon(priority) {
|
|
21
|
+
switch (priority) {
|
|
22
|
+
case 'high': return '\u{1F534} ';
|
|
23
|
+
case 'medium': return '\u{1F7E1} ';
|
|
24
|
+
default: return '';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Dim effort badge like `▪ L`, or empty string */
|
|
28
|
+
export function formatEffort(effort) {
|
|
29
|
+
if (!effort)
|
|
30
|
+
return '';
|
|
31
|
+
return pc.dim(`\u25AA ${effort.toUpperCase()}`);
|
|
32
|
+
}
|
|
33
|
+
/** Dim feedback count like `💬 3`, or empty string */
|
|
34
|
+
export function formatFeedback(count) {
|
|
35
|
+
if (!count)
|
|
36
|
+
return '';
|
|
37
|
+
return pc.dim(`\u{1F4AC} ${count}`);
|
|
38
|
+
}
|
|
39
|
+
/** Dim labels joined with ` · `, or empty string */
|
|
40
|
+
export function formatLabels(labels) {
|
|
41
|
+
if (!labels || labels.length === 0)
|
|
42
|
+
return '';
|
|
43
|
+
return pc.dim(labels.join(' \u00B7 '));
|
|
44
|
+
}
|
|
45
|
+
/** 10-char progress bar: green filled, dim empty, with count */
|
|
46
|
+
export function formatSubtaskProgress(progress) {
|
|
47
|
+
if (!progress || progress.total === 0)
|
|
48
|
+
return '';
|
|
49
|
+
const { done, total } = progress;
|
|
50
|
+
const width = 10;
|
|
51
|
+
const filled = Math.round((done / total) * width);
|
|
52
|
+
const empty = width - filled;
|
|
53
|
+
const bar = pc.green('\u2501'.repeat(filled)) + pc.dim('\u2591'.repeat(empty));
|
|
54
|
+
return `${bar} ${done}/${total}`;
|
|
55
|
+
}
|
|
56
|
+
/** Relative time string from date, no dependencies */
|
|
57
|
+
export function relativeTime(date) {
|
|
58
|
+
if (!date)
|
|
59
|
+
return '';
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const then = date instanceof Date ? date.getTime() : new Date(date).getTime();
|
|
62
|
+
if (isNaN(then))
|
|
63
|
+
return '';
|
|
64
|
+
const diffMs = now - then;
|
|
65
|
+
if (diffMs < 0)
|
|
66
|
+
return 'just now';
|
|
67
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
68
|
+
if (seconds < 60)
|
|
69
|
+
return 'just now';
|
|
70
|
+
const minutes = Math.floor(seconds / 60);
|
|
71
|
+
if (minutes < 60)
|
|
72
|
+
return `${minutes}m ago`;
|
|
73
|
+
const hours = Math.floor(minutes / 60);
|
|
74
|
+
if (hours < 24)
|
|
75
|
+
return `${hours}h ago`;
|
|
76
|
+
const days = Math.floor(hours / 24);
|
|
77
|
+
if (days < 14)
|
|
78
|
+
return `${days}d ago`;
|
|
79
|
+
const weeks = Math.floor(days / 7);
|
|
80
|
+
if (weeks < 8)
|
|
81
|
+
return `${weeks}w ago`;
|
|
82
|
+
const months = Math.floor(days / 30);
|
|
83
|
+
if (months < 12)
|
|
84
|
+
return `${months}mo ago`;
|
|
85
|
+
const years = Math.floor(days / 365);
|
|
86
|
+
return `${years}y ago`;
|
|
87
|
+
}
|
|
88
|
+
/** Returns a picocolors function for the given status */
|
|
89
|
+
export function statusColor(status) {
|
|
90
|
+
switch (status) {
|
|
91
|
+
case 'done': return pc.green;
|
|
92
|
+
case 'in_progress': return pc.yellow;
|
|
93
|
+
default: return pc.dim;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Box-drawing section header: `┌─ Label ───────────────┐` */
|
|
97
|
+
export function sectionHeader(label, width = 40) {
|
|
98
|
+
const inner = `\u2500 ${label} `;
|
|
99
|
+
const remaining = Math.max(0, width - inner.length - 2); // 2 for ┌ and ┐
|
|
100
|
+
return `\u250C${inner}${'\u2500'.repeat(remaining)}\u2510`;
|
|
101
|
+
}
|
|
102
|
+
/** Compact one-liner: `🔴 ✨ #a1b2c3d4 Title ▪ L` */
|
|
103
|
+
export function formatTaskLine(task) {
|
|
104
|
+
const priority = getPriorityIcon(task.priority);
|
|
105
|
+
const icon = getTypeIcon(task.type);
|
|
106
|
+
const id = shortId(task.id);
|
|
107
|
+
const effort = formatEffort(task.effort);
|
|
108
|
+
const parts = [
|
|
109
|
+
`${priority}${icon} ${id} ${task.title}`,
|
|
110
|
+
];
|
|
111
|
+
if (effort)
|
|
112
|
+
parts.push(effort);
|
|
113
|
+
return parts.join(' ');
|
|
114
|
+
}
|
|
115
|
+
/** Dim metadata second line with segments joined by ` · ` */
|
|
116
|
+
export function formatTaskMeta(task) {
|
|
117
|
+
const segments = [];
|
|
118
|
+
if (task.quarter) {
|
|
119
|
+
segments.push(pc.dim(task.quarter));
|
|
120
|
+
}
|
|
121
|
+
if (task.hasImplementationPlan) {
|
|
122
|
+
segments.push(pc.dim('has plan'));
|
|
123
|
+
}
|
|
124
|
+
const progress = formatSubtaskProgress(task.subtaskProgress);
|
|
125
|
+
if (progress) {
|
|
126
|
+
segments.push(progress);
|
|
127
|
+
}
|
|
128
|
+
const feedback = formatFeedback(task.feedbackCount);
|
|
129
|
+
if (feedback) {
|
|
130
|
+
segments.push(feedback);
|
|
131
|
+
}
|
|
132
|
+
const labels = formatLabels(task.labels);
|
|
133
|
+
if (labels) {
|
|
134
|
+
segments.push(labels);
|
|
135
|
+
}
|
|
136
|
+
if (segments.length === 0)
|
|
137
|
+
return '';
|
|
138
|
+
return segments.join(` ${pc.dim('\u00B7')} `);
|
|
139
|
+
}
|
package/dist/ui/task-picker.js
CHANGED
|
@@ -1,52 +1,35 @@
|
|
|
1
1
|
import { select, confirm, input, Separator } from '@inquirer/prompts';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
|
-
|
|
4
|
-
switch (type) {
|
|
5
|
-
case 'bug': return pc.red('bug');
|
|
6
|
-
case 'feature': return pc.green('feature');
|
|
7
|
-
case 'improvement': return pc.blue('improvement');
|
|
8
|
-
default: return pc.gray('task');
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
function getPriorityIcon(priority) {
|
|
12
|
-
switch (priority) {
|
|
13
|
-
case 'high': return pc.red('!');
|
|
14
|
-
case 'medium': return pc.yellow('~');
|
|
15
|
-
default: return pc.dim('-');
|
|
16
|
-
}
|
|
17
|
-
}
|
|
3
|
+
import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort, formatTaskMeta, sectionHeader, relativeTime, } from './format.js';
|
|
18
4
|
function formatTaskChoice(choice) {
|
|
19
5
|
const { task } = choice;
|
|
20
|
-
const typeIcon = getTypeIcon(task.type);
|
|
21
6
|
const priorityIcon = getPriorityIcon(task.priority);
|
|
7
|
+
const typeIcon = getTypeIcon(task.type);
|
|
8
|
+
const id = shortId(task.id);
|
|
9
|
+
const effort = formatEffort(task.effort);
|
|
10
|
+
const firstLine = `${priorityIcon}${typeIcon} ${id} ${task.title}${effort ? ` ${effort}` : ''}`;
|
|
22
11
|
if (choice.type === 'in_progress') {
|
|
23
12
|
const worktreeName = choice.worktree.path.split('/').pop() || choice.worktree.path;
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
const time = relativeTime(choice.worktree.createdAt);
|
|
14
|
+
const metaParts = [worktreeName];
|
|
15
|
+
if (time)
|
|
16
|
+
metaParts.push(time);
|
|
17
|
+
let description = `${firstLine}\n ${pc.dim(metaParts.join(' \u00B7 '))}`;
|
|
26
18
|
if (choice.lastNote) {
|
|
27
|
-
// Truncate long notes
|
|
28
19
|
const note = choice.lastNote.length > 60 ? choice.lastNote.slice(0, 60) + '...' : choice.lastNote;
|
|
29
|
-
description += `\n
|
|
20
|
+
description += `\n ${pc.dim(`Last: "${note}"`)}`;
|
|
30
21
|
}
|
|
31
22
|
return description;
|
|
32
23
|
}
|
|
33
24
|
if (choice.type === 'locked') {
|
|
34
|
-
|
|
35
|
-
description += pc.yellow(` [locked by ${choice.lockedBy}]`);
|
|
36
|
-
return description;
|
|
25
|
+
return `${firstLine}\n ${pc.yellow(`locked by ${choice.lockedBy}`)}`;
|
|
37
26
|
}
|
|
38
27
|
// Available task
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
if (task.hasImplementationPlan) {
|
|
44
|
-
description += pc.dim(' [plan]');
|
|
45
|
-
}
|
|
46
|
-
if (task.subtaskProgress) {
|
|
47
|
-
description += pc.dim(` [${task.subtaskProgress.done}/${task.subtaskProgress.total}]`);
|
|
28
|
+
const meta = formatTaskMeta(task);
|
|
29
|
+
if (meta) {
|
|
30
|
+
return `${firstLine}\n ${meta}`;
|
|
48
31
|
}
|
|
49
|
-
return
|
|
32
|
+
return firstLine;
|
|
50
33
|
}
|
|
51
34
|
export async function pickTask(options) {
|
|
52
35
|
const { api, worktrees, typeFilter, statusFilter } = options;
|
|
@@ -97,7 +80,7 @@ export async function pickTask(options) {
|
|
|
97
80
|
}
|
|
98
81
|
const choices = [];
|
|
99
82
|
if (inProgressChoices.length > 0) {
|
|
100
|
-
choices.push(new Separator(
|
|
83
|
+
choices.push(new Separator(`\n${sectionHeader(`In Progress (${inProgressChoices.length})`)}`));
|
|
101
84
|
for (const choice of inProgressChoices) {
|
|
102
85
|
choices.push({
|
|
103
86
|
name: formatTaskChoice(choice),
|
|
@@ -106,7 +89,7 @@ export async function pickTask(options) {
|
|
|
106
89
|
}
|
|
107
90
|
}
|
|
108
91
|
if (availableChoices.length > 0) {
|
|
109
|
-
choices.push(new Separator(
|
|
92
|
+
choices.push(new Separator(`\n${sectionHeader(`Available (${availableChoices.length})`)}`));
|
|
110
93
|
for (const choice of availableChoices) {
|
|
111
94
|
choices.push({
|
|
112
95
|
name: formatTaskChoice(choice),
|
|
@@ -115,7 +98,7 @@ export async function pickTask(options) {
|
|
|
115
98
|
}
|
|
116
99
|
}
|
|
117
100
|
if (lockedChoices.length > 0) {
|
|
118
|
-
choices.push(new Separator(
|
|
101
|
+
choices.push(new Separator(`\n${sectionHeader(`Locked (${lockedChoices.length})`)}`));
|
|
119
102
|
for (const choice of lockedChoices) {
|
|
120
103
|
choices.push({
|
|
121
104
|
name: formatTaskChoice(choice),
|
|
@@ -139,7 +122,7 @@ export async function pickTask(options) {
|
|
|
139
122
|
const selected = await select({
|
|
140
123
|
message: 'Select a task to work on:',
|
|
141
124
|
choices: choices,
|
|
142
|
-
pageSize:
|
|
125
|
+
pageSize: 20,
|
|
143
126
|
});
|
|
144
127
|
if (selected.type === 'create_new') {
|
|
145
128
|
return handleCreateNewTask(api);
|
|
@@ -190,7 +173,7 @@ async function handleCreateNewTask(api) {
|
|
|
190
173
|
});
|
|
191
174
|
console.log(pc.dim('\nCreating task in Damper...'));
|
|
192
175
|
const task = await api.createTask(title.trim(), type);
|
|
193
|
-
console.log(pc.green(`✓ Created task #${task.id}: ${task.title}`));
|
|
176
|
+
console.log(pc.green(`✓ Created task #${shortIdRaw(task.id)}: ${task.title}`));
|
|
194
177
|
return {
|
|
195
178
|
task,
|
|
196
179
|
isResume: false,
|