create-walle 0.9.16 → 0.9.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-walle",
3
- "version": "0.9.16",
3
+ "version": "0.9.17",
4
4
  "description": "CTM + Wall-E \u2014 AI coding dashboard and personal digital twin agent. Multi-agent terminal for Claude Code, Codex, Gemini, Aider, OpenCode, and more, plus prompt editor, task queue, and an agent that learns from Slack, email & calendar.",
5
5
  "bin": {
6
6
  "create-walle": "bin/create-walle.js"
@@ -1,9 +1,16 @@
1
1
  #!/bin/bash
2
- # Run this after authenticating gh as the ShanniLi account:
2
+ # Run this after authenticating gh for the repository owner account:
3
3
  # gh auth login
4
4
  # Or manually set these in GitHub Settings > General
5
5
 
6
- gh repo edit ShanniLi/tools \
6
+ if [ -z "${WALLE_GITHUB_REPO:-}" ]; then
7
+ echo "Set WALLE_GITHUB_REPO=owner/repo before running this script."
8
+ exit 1
9
+ fi
10
+
11
+ REPO="$WALLE_GITHUB_REPO"
12
+
13
+ gh repo edit "$REPO" \
7
14
  --description "Wall-E — your personal digital twin. AI agent that learns from Slack, email & calendar. Includes CTM dashboard." \
8
15
  --homepage "https://walle.sh" \
9
16
  --add-topic "ai" \
@@ -15,4 +22,4 @@ gh repo edit ShanniLi/tools \
15
22
  --add-topic "nodejs"
16
23
 
17
24
  echo "Done. Set social preview image manually at:"
18
- echo " https://github.com/ShanniLi/tools/settings"
25
+ echo " https://github.com/${REPO}/settings"
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('crypto');
4
+ const fs = require('fs');
4
5
 
5
6
  function stripAnsi(text) {
6
7
  return String(text || '')
@@ -45,8 +46,84 @@ function fingerprintFinalText(text) {
45
46
  return crypto.createHash('sha1').update(String(text || '')).digest('hex').slice(0, 16);
46
47
  }
47
48
 
49
+ function _codexAssistantOutputText(content) {
50
+ if (!Array.isArray(content)) return '';
51
+ return content
52
+ .filter((block) => block && block.type === 'output_text' && block.text)
53
+ .map((block) => block.text)
54
+ .join('\n')
55
+ .trim();
56
+ }
57
+
58
+ function codexFinalTextFromEntry(entry) {
59
+ if (!entry || typeof entry !== 'object') return null;
60
+ if (entry.type === 'event_msg' && entry.payload?.type === 'task_complete') {
61
+ const text = String(entry.payload.last_agent_message || '').trim();
62
+ if (text) {
63
+ return {
64
+ text,
65
+ timestamp: entry.timestamp || entry.payload.timestamp || '',
66
+ source: 'task_complete',
67
+ };
68
+ }
69
+ }
70
+ if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'assistant') {
71
+ const text = _codexAssistantOutputText(entry.payload.content);
72
+ if (text) {
73
+ return {
74
+ text,
75
+ timestamp: entry.timestamp || '',
76
+ source: 'assistant_message',
77
+ };
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+
83
+ function latestCodexFinalTextFromJsonlText(text) {
84
+ const lines = String(text || '').split(/\r?\n/);
85
+ for (let i = lines.length - 1; i >= 0; i--) {
86
+ const line = lines[i];
87
+ if (!line || !line.trim()) continue;
88
+ let entry;
89
+ try { entry = JSON.parse(line); } catch { continue; }
90
+ const final = codexFinalTextFromEntry(entry);
91
+ if (final) return final;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ function latestCodexFinalTextFromJsonlFile(filePath, options = {}) {
97
+ if (!filePath) return null;
98
+ const maxTailBytes = Math.max(1024, Number(options.maxTailBytes || 8 * 1024 * 1024));
99
+ let st;
100
+ try { st = fs.statSync(filePath); } catch { return null; }
101
+ if (!st || !st.isFile() || st.size <= 0) return null;
102
+ const readSize = Math.min(st.size, maxTailBytes);
103
+ const start = st.size - readSize;
104
+ let fd = null;
105
+ try {
106
+ fd = fs.openSync(filePath, 'r');
107
+ const buf = Buffer.alloc(readSize);
108
+ fs.readSync(fd, buf, 0, readSize, start);
109
+ let text = buf.toString('utf8');
110
+ if (start > 0) {
111
+ const firstNewline = text.indexOf('\n');
112
+ if (firstNewline >= 0) text = text.slice(firstNewline + 1);
113
+ }
114
+ return latestCodexFinalTextFromJsonlText(text);
115
+ } catch {
116
+ return null;
117
+ } finally {
118
+ try { if (fd != null) fs.closeSync(fd); } catch {}
119
+ }
120
+ }
121
+
48
122
  module.exports = {
123
+ codexFinalTextFromEntry,
49
124
  fingerprintFinalText,
125
+ latestCodexFinalTextFromJsonlFile,
126
+ latestCodexFinalTextFromJsonlText,
50
127
  markerForFinalText,
51
128
  normalizeForTerminalSearch,
52
129
  terminalContainsFinalText,
@@ -48,11 +48,127 @@ function progressText(progress) {
48
48
  if (!progress) return '';
49
49
  if (typeof progress === 'string') return progress;
50
50
  if (typeof progress === 'object') {
51
- return progress.summary || progress.next || progress.phase || '';
51
+ if (progress.summary) return progress.summary;
52
+ if (Array.isArray(progress.bullets) && progress.bullets.length) {
53
+ return progress.bullets.filter(Boolean).slice(-1)[0] || '';
54
+ }
55
+ return progress.next || progress.phase || '';
52
56
  }
53
57
  return String(progress);
54
58
  }
55
59
 
60
+ function sourceForValue(value, fallback = '') {
61
+ return value && typeof value === 'object' && value.source
62
+ ? String(value.source)
63
+ : fallback;
64
+ }
65
+
66
+ function freshnessForValue(value, fallback = '') {
67
+ return value && typeof value === 'object' && value.freshness
68
+ ? String(value.freshness)
69
+ : fallback;
70
+ }
71
+
72
+ function confidenceForValue(value, fallback = '') {
73
+ return value && typeof value === 'object' && value.confidence
74
+ ? String(value.confidence)
75
+ : fallback;
76
+ }
77
+
78
+ function timestampForValue(value) {
79
+ if (!value || typeof value !== 'object') return 0;
80
+ return value.updatedAt || value.timestamp || value.promptTimestamp || value.aiUpdatedAt || 0;
81
+ }
82
+
83
+ function goalForSummary(session, summary = {}) {
84
+ const candidates = [
85
+ {
86
+ value: summary?.currentTask,
87
+ source: sourceForValue(summary?.currentTask, 'current-task'),
88
+ freshness: freshnessForValue(summary?.currentTask, ''),
89
+ confidence: confidenceForValue(summary?.currentTask, ''),
90
+ updatedAt: timestampForValue(summary?.currentTask),
91
+ },
92
+ {
93
+ value: summary?.intent,
94
+ source: sourceForValue(summary?.intent, 'intent'),
95
+ freshness: freshnessForValue(summary?.intent, ''),
96
+ confidence: confidenceForValue(summary?.intent, ''),
97
+ updatedAt: timestampForValue(summary?.intent),
98
+ },
99
+ {
100
+ value: summary?.summary,
101
+ source: summary?.aiSummary?.source || (summary?.summary ? 'ai-summary' : ''),
102
+ freshness: summary?.aiSummary?.status || '',
103
+ confidence: summary?.summary ? 'medium' : '',
104
+ updatedAt: summary?.aiSummary?.updatedAt || 0,
105
+ },
106
+ {
107
+ value: summary?.displayPrompt,
108
+ source: 'prompt-fallback',
109
+ freshness: 'fallback',
110
+ confidence: 'medium',
111
+ updatedAt: summary?.displayPrompt?.timestamp || summary?.displayPrompt?.updatedAt || 0,
112
+ },
113
+ {
114
+ value: summary?.lastPrompt,
115
+ source: 'prompt-fallback',
116
+ freshness: 'fallback',
117
+ confidence: 'medium',
118
+ updatedAt: summary?.lastPrompt?.timestamp || summary?.lastPrompt?.updatedAt || 0,
119
+ },
120
+ {
121
+ value: session?.label,
122
+ source: 'title-fallback',
123
+ freshness: 'fallback',
124
+ confidence: 'low',
125
+ updatedAt: session?.lastActivity || session?.modifiedAt || session?.createdAt || 0,
126
+ },
127
+ ];
128
+
129
+ for (const candidate of candidates) {
130
+ const text = truncateText(valueText(candidate.value), 170);
131
+ if (!text) continue;
132
+ return {
133
+ text,
134
+ source: candidate.source || 'unknown',
135
+ freshness: candidate.freshness || '',
136
+ confidence: candidate.confidence || '',
137
+ updatedAt: candidate.updatedAt || 0,
138
+ };
139
+ }
140
+
141
+ return {
142
+ text: '',
143
+ source: 'missing',
144
+ freshness: 'missing',
145
+ confidence: 'low',
146
+ updatedAt: 0,
147
+ };
148
+ }
149
+
150
+ function progressForSummary(summary = {}) {
151
+ const progress = summary?.progress || '';
152
+ const text = truncateText(progressText(progress), 190);
153
+ if (!progress || typeof progress !== 'object') {
154
+ return {
155
+ text,
156
+ source: progress ? 'text' : 'missing',
157
+ phase: '',
158
+ next: '',
159
+ updatedAt: 0,
160
+ };
161
+ }
162
+
163
+ return {
164
+ text,
165
+ source: progress.source || (Array.isArray(progress.bullets) && progress.bullets.length ? 'assistant-events' : 'progress'),
166
+ phase: progress.phase || '',
167
+ next: truncateText(progress.next || '', 160),
168
+ updatedAt: progress.updatedAt || 0,
169
+ };
170
+ }
171
+
56
172
  function toMs(value) {
57
173
  if (!value) return 0;
58
174
  if (typeof value === 'number' && Number.isFinite(value)) return value;
@@ -194,7 +310,7 @@ function prependEvidence(evidence, items) {
194
310
  return out.slice(0, 5);
195
311
  }
196
312
 
197
- function baseEvidence({ session, status, summary, now, intentText, progress }) {
313
+ function baseEvidence({ session, status, summary, now, intentText, progress, goalSource }) {
198
314
  const evidence = [];
199
315
  if (status === 'waiting_input') evidence.push('waiting input');
200
316
  else if (status === 'waiting') evidence.push('waiting');
@@ -205,7 +321,9 @@ function baseEvidence({ session, status, summary, now, intentText, progress }) {
205
321
  if (wtEvidence) evidence.push(wtEvidence);
206
322
  const age = ageLabel(now, latestActivity(session, summary));
207
323
  if (age) evidence.push(`last activity ${age}`);
208
- if (intentText && summary?.intent?.source === 'prompt-fallback') {
324
+ const promptBacked = [goalSource, summary?.currentTask?.source, summary?.intent?.source]
325
+ .some(source => ['latest-prompt', 'prompt-fallback'].includes(source));
326
+ if (intentText && promptBacked) {
209
327
  evidence.push(`prompt: ${truncateText(intentText, 80)}`);
210
328
  }
211
329
  const progressPhase = progress && typeof progress === 'object' ? progress.phase : '';
@@ -219,7 +337,11 @@ function latestActivity(session, summary) {
219
337
  const values = [
220
338
  summary?.lastActivity,
221
339
  summary?.progress?.updatedAt,
340
+ summary?.currentTask?.updatedAt,
341
+ summary?.currentTask?.timestamp,
342
+ summary?.currentTask?.promptTimestamp,
222
343
  summary?.intent?.updatedAt,
344
+ summary?.aiSummary?.updatedAt,
223
345
  session?.lastPtyActivity,
224
346
  session?.lastActivity,
225
347
  session?.modifiedAt,
@@ -238,10 +360,11 @@ function classifySessionStandup(session, signals = {}, now = Date.now()) {
238
360
  const summary = signals.summary || {};
239
361
  const streamStatus = signals.status || {};
240
362
  const status = standupStatusForSession(session, streamStatus, summary);
241
- const intent = summary.intent || summary.summary || summary.displayPrompt || summary.lastPrompt || session?.label || '';
242
- const intentString = truncateText(valueText(intent), 160);
363
+ const goal = goalForSummary(session, summary);
364
+ const intentString = goal.text;
243
365
  const progress = summary.progress || '';
244
- const progressString = truncateText(progressText(progress), 180);
366
+ const progressInfo = progressForSummary(summary);
367
+ const progressString = progressInfo.text;
245
368
  const summaryText = valueText(summary.summary) || intentString;
246
369
  const lastActivityMs = latestActivity(session, summary);
247
370
  const ageMs = lastActivityMs ? Math.max(0, now - lastActivityMs) : null;
@@ -304,7 +427,7 @@ function classifySessionStandup(session, signals = {}, now = Date.now()) {
304
427
  confidence = 'medium';
305
428
  }
306
429
 
307
- let evidence = baseEvidence({ session, status, summary, now, intentText: intentString, progress });
430
+ let evidence = baseEvidence({ session, status, summary, now, intentText: intentString, progress, goalSource: goal.source });
308
431
  if ((failed || warning) && attention?.evidence?.length) {
309
432
  evidence = prependEvidence(evidence, attention.evidence);
310
433
  }
@@ -338,7 +461,15 @@ function classifySessionStandup(session, signals = {}, now = Date.now()) {
338
461
  confidence,
339
462
  evidence,
340
463
  attention: attention && attention.severity !== 'none' ? attention : null,
464
+ goal: goal.text,
465
+ goalSource: goal.source,
466
+ goalFreshness: goal.freshness,
467
+ goalConfidence: goal.confidence,
341
468
  intent: intentString,
469
+ progressSummary: progressString,
470
+ progressSource: progressInfo.source,
471
+ progressPhase: progressInfo.phase,
472
+ progressNext: progressInfo.next,
342
473
  progress: progressString,
343
474
  lastActivity: toIso(lastActivityMs),
344
475
  ageMs,
@@ -55,6 +55,34 @@
55
55
  outline: none;
56
56
  }
57
57
  .sidebar-header input:focus { border-color: var(--accent); }
58
+ .prompt-filter-row {
59
+ display: grid;
60
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 32px 32px 32px;
61
+ gap: 4px;
62
+ align-items: center;
63
+ min-width: 0;
64
+ }
65
+ .prompt-filter-row .prompt-filter-select {
66
+ min-width: 0;
67
+ width: 100%;
68
+ height: 28px;
69
+ }
70
+ .prompt-filter-row .prompt-filter-icon-btn {
71
+ width: 32px;
72
+ min-width: 32px;
73
+ height: 28px;
74
+ padding: 0;
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ overflow: hidden;
79
+ flex: 0 0 auto;
80
+ line-height: 1;
81
+ }
82
+ .prompt-filter-row .prompt-create-folder-btn {
83
+ font-size: 15px;
84
+ font-weight: 700;
85
+ }
58
86
  .sidebar-header input.ai-active {
59
87
  border-color: var(--purple, #bb9af7);
60
88
  background: rgba(187, 154, 247, 0.08);
@@ -288,12 +288,23 @@
288
288
  .walle-model-footer {
289
289
  flex: 0 0 auto;
290
290
  display: flex;
291
- justify-content: flex-end;
291
+ align-items: center;
292
+ justify-content: space-between;
293
+ gap: 10px;
292
294
  padding: 8px 10px;
293
295
  border-top: 1px solid #2f354f;
294
296
  background: rgba(255,255,255,0.02);
295
297
  }
298
+ .walle-model-source-note {
299
+ min-width: 0;
300
+ color: #8f98c7;
301
+ font-size: 11px;
302
+ overflow: hidden;
303
+ text-overflow: ellipsis;
304
+ white-space: nowrap;
305
+ }
296
306
  .walle-model-legacy-toggle {
307
+ flex: 0 0 auto;
297
308
  background: transparent;
298
309
  color: #8f98c7;
299
310
  border: 1px solid #3b4261;
@@ -2477,6 +2477,42 @@
2477
2477
  color: var(--fg);
2478
2478
  font-weight: 600;
2479
2479
  }
2480
+ .standup-card-signals {
2481
+ display: flex;
2482
+ flex-direction: column;
2483
+ gap: 6px;
2484
+ min-width: 0;
2485
+ }
2486
+ .standup-card-line {
2487
+ display: grid;
2488
+ grid-template-columns: 58px minmax(0, 1fr);
2489
+ gap: 8px;
2490
+ align-items: start;
2491
+ min-width: 0;
2492
+ font-size: 12px;
2493
+ line-height: 1.35;
2494
+ }
2495
+ .standup-card-line-label {
2496
+ color: var(--fg-dim);
2497
+ font-size: 10px;
2498
+ font-weight: 700;
2499
+ text-transform: uppercase;
2500
+ letter-spacing: 0;
2501
+ padding-top: 1px;
2502
+ white-space: nowrap;
2503
+ }
2504
+ .standup-card-line-text {
2505
+ color: var(--fg);
2506
+ min-width: 0;
2507
+ overflow-wrap: anywhere;
2508
+ }
2509
+ .standup-card-line.is-muted .standup-card-line-text {
2510
+ color: var(--fg-dim);
2511
+ }
2512
+ .standup-card-recommendation {
2513
+ padding-top: 7px;
2514
+ border-top: 1px solid rgba(255,255,255,0.07);
2515
+ }
2480
2516
  .standup-evidence {
2481
2517
  display: flex;
2482
2518
  flex-wrap: wrap;
@@ -4324,8 +4360,8 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
4324
4360
  <div id="pe-ac-dropdown" style="display:none;position:absolute;top:100%;left:0;right:0;background:var(--bg-light, #24283b);border:1px solid var(--border);border-radius:6px;z-index:200;max-height:250px;overflow-y:auto;box-shadow:0 8px 24px rgba(0,0,0,0.5);"></div>
4325
4361
  <button id="pe-ai-search-btn" class="ai-toggle" onclick="PE.toggleAiSearch()" title="AI-powered semantic search">AI</button>
4326
4362
  </div>
4327
- <div style="display:flex;gap:4px;">
4328
- <select id="pe-context-filter" onchange="PE.filterPrompts()" style="flex:1;background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:3px 6px;border-radius:4px;font-size:10px;">
4363
+ <div class="prompt-filter-row">
4364
+ <select id="pe-context-filter" class="prompt-filter-select" onchange="PE.filterPrompts()" style="flex:1;background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:3px 6px;border-radius:4px;font-size:10px;">
4329
4365
  <option value="">All Domains</option>
4330
4366
  <option value="coding">Coding</option>
4331
4367
  <option value="ops">Ops &amp; Infra</option>
@@ -4335,7 +4371,7 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
4335
4371
  <option value="meta">Meta / CTM</option>
4336
4372
  <option value="general">General</option>
4337
4373
  </select>
4338
- <select id="pe-lifecycle-filter" onchange="PE.filterPrompts()" style="flex:1;background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:3px 6px;border-radius:4px;font-size:10px;">
4374
+ <select id="pe-lifecycle-filter" class="prompt-filter-select" onchange="PE.filterPrompts()" style="flex:1;background:var(--bg);color:var(--fg);border:1px solid var(--border);padding:3px 6px;border-radius:4px;font-size:10px;">
4339
4375
  <option value="">All Status</option>
4340
4376
  <option value="draft">Draft</option>
4341
4377
  <option value="used">Used</option>
@@ -4343,9 +4379,9 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
4343
4379
  <option value="template">Template</option>
4344
4380
  <option value="archived">Archived</option>
4345
4381
  </select>
4346
- <button class="btn small" id="pe-star-filter-btn" onclick="PE.filterPrompts('starred')" title="Starred only">&#9733;</button>
4347
- <button class="btn small" id="pe-select-mode-btn" onclick="PE.toggleSelectMode()" title="Select multiple prompts">&#9744;</button>
4348
- <button class="btn small" onclick="PE.createNewFolder()" title="Create new folder">+ Folder</button>
4382
+ <button class="btn small prompt-filter-icon-btn" id="pe-star-filter-btn" onclick="PE.filterPrompts('starred')" title="Starred only">&#9733;</button>
4383
+ <button class="btn small prompt-filter-icon-btn" id="pe-select-mode-btn" onclick="PE.toggleSelectMode()" title="Select multiple prompts">&#9744;</button>
4384
+ <button class="btn small prompt-filter-icon-btn prompt-create-folder-btn" onclick="PE.createNewFolder()" title="Create new folder" aria-label="Create new folder">+</button>
4349
4385
  </div>
4350
4386
  </div>
4351
4387
  <div id="pe-bulk-bar" style="display:none;padding:6px 8px;background:var(--bg-lighter);border-bottom:1px solid var(--border);display:none;align-items:center;gap:6px;font-size:11px;">
@@ -4854,6 +4890,11 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
4854
4890
  <input type="hidden" id="ns-agent" value="claude:" />
4855
4891
  <label>Working Directory</label>
4856
4892
  <input id="ns-cwd" type="text" placeholder="~/" onblur="_maybeRecommendWorktreeForNewSession(false)" />
4893
+ <label>Session Title (optional)</label>
4894
+ <input id="ns-label" type="text" placeholder="Auto-generated" />
4895
+ <div style="font-size:11px;color:var(--fg-dim,#565f89);margin:5px 0 10px;">
4896
+ Shown on the tab and sidebar. Rename later by double-clicking the session tab title.
4897
+ </div>
4857
4898
  <div id="ns-worktree-row" style="margin-top:8px;display:flex;align-items:center;gap:8px;">
4858
4899
  <label style="display:flex;align-items:center;gap:6px;margin:0;cursor:pointer;font-size:13px;color:var(--fg-dim,#a9b1d6);">
4859
4900
  <input type="checkbox" id="ns-worktree" style="accent-color:var(--accent,#7aa2f7);width:15px;height:15px;" onchange="toggleWorktreeFields()" />
@@ -4863,7 +4904,10 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
4863
4904
  </div>
4864
4905
  <div id="ns-worktree-fields" style="display:none;margin-top:6px;">
4865
4906
  <label style="font-size:12px;">Branch Name</label>
4866
- <input id="ns-worktree-name" type="text" placeholder="Auto-generated from label" style="font-size:13px;" />
4907
+ <input id="ns-worktree-name" type="text" placeholder="Auto-generated from session title" style="font-size:13px;" />
4908
+ <div style="font-size:11px;color:var(--fg-dim,#565f89);margin:5px 0 0;">
4909
+ Leave blank to generate a branch-safe name from the session title.
4910
+ </div>
4867
4911
  </div>
4868
4912
  <div id="ns-custom-fields" style="display:none">
4869
4913
  <label>Command</label>
@@ -4871,8 +4915,6 @@ url = "http://localhost:<span id="setup-mcp-port-display-toml">3457</span>/mcp"<
4871
4915
  <label>Arguments (comma-separated)</label>
4872
4916
  <input id="ns-args" type="text" placeholder="" />
4873
4917
  </div>
4874
- <label>Label (optional)</label>
4875
- <input id="ns-label" type="text" placeholder="Auto-generated" />
4876
4918
  <div class="btn-row">
4877
4919
  <button class="btn" onclick="closeModal('new-session-modal')">Cancel</button>
4878
4920
  <button class="btn primary" onclick="createSession()">Create</button>
@@ -7810,11 +7852,27 @@ function renderStandupLane(lane) {
7810
7852
  `;
7811
7853
  }
7812
7854
 
7855
+ function renderStandupCardLine(label, text, opts) {
7856
+ opts = opts || {};
7857
+ const value = String(text || '').trim();
7858
+ if (!value) return '';
7859
+ const muted = opts.muted ? ' is-muted' : '';
7860
+ return `
7861
+ <div class="standup-card-line${muted}" data-standup-line="${standupEsc(label.toLowerCase())}">
7862
+ <div class="standup-card-line-label">${standupEsc(label)}</div>
7863
+ <div class="standup-card-line-text">${standupEsc(value)}</div>
7864
+ </div>
7865
+ `;
7866
+ }
7867
+
7813
7868
  function renderStandupCard(card) {
7814
7869
  const subtitle = [card.agent, card.model || card.provider, card.branch].filter(Boolean).join(' / ');
7815
- const progress = card.progress || card.intent || '';
7870
+ const goal = card.goal || card.intent || '';
7871
+ const progress = card.progressSummary || card.progress || '';
7816
7872
  const canReview = card.capabilities && card.capabilities.review;
7817
7873
  const chips = (card.evidence || []).map(item => `<span class="standup-chip" title="${standupEsc(item)}">${standupEsc(item)}</span>`).join('');
7874
+ const goalLine = renderStandupCardLine('Goal', goal);
7875
+ const progressLine = renderStandupCardLine('Progress', progress || 'No progress signal yet.', { muted: !progress });
7818
7876
  return `
7819
7877
  <article class="standup-card">
7820
7878
  <div class="standup-card-top">
@@ -7824,8 +7882,8 @@ function renderStandupCard(card) {
7824
7882
  </div>
7825
7883
  <span class="standup-badge ${standupStatusClass(card.status)}">${standupEsc(card.status || 'unknown')}</span>
7826
7884
  </div>
7827
- <div class="standup-card-text"><strong>${standupEsc(card.actionLabel || 'Next')}</strong> ${standupEsc(card.recommendation || '')}</div>
7828
- ${progress ? `<div class="standup-card-text">${standupEsc(progress)}</div>` : ''}
7885
+ ${goalLine || progressLine ? `<div class="standup-card-signals">${goalLine}${progressLine}</div>` : ''}
7886
+ <div class="standup-card-text standup-card-recommendation"><strong>${standupEsc(card.actionLabel || 'Next')}</strong> ${standupEsc(card.recommendation || '')}</div>
7829
7887
  ${chips ? `<div class="standup-evidence">${chips}</div>` : ''}
7830
7888
  <div class="standup-card-actions">
7831
7889
  <button class="standup-action-btn primary" type="button" data-standup-action="open" data-session-id="${standupEsc(card.id)}">Open</button>
@@ -14211,7 +14269,7 @@ function createSession() {
14211
14269
  if (useWorktree && cwd) {
14212
14270
  var wtName = document.getElementById('ns-worktree-name').value.trim();
14213
14271
  if (!wtName) {
14214
- // Auto-generate from label or timestamp
14272
+ // Auto-generate from session title or timestamp
14215
14273
  wtName = (label || '').trim().toLowerCase().replace(/[^a-z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
14216
14274
  if (!wtName) wtName = 'wt-' + Date.now().toString(36);
14217
14275
  }