codexmate 0.1.4 → 0.1.6

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.
@@ -204,6 +204,17 @@ function extractMessageFromRecord(record, source) {
204
204
  }
205
205
  return null;
206
206
  }
207
+ if (source === 'pi') {
208
+ if (record.type === 'message' && record.message && typeof record.message === 'object') {
209
+ const role = normalizeRole(record.message.role);
210
+ const text = extractMessageText(record.message.content);
211
+ if (!role || !text) {
212
+ return null;
213
+ }
214
+ return { role, text };
215
+ }
216
+ return null;
217
+ }
207
218
 
208
219
  const role = normalizeRole(record.type);
209
220
  if (!role) {
@@ -271,6 +282,14 @@ function applySessionDetailRecordMetadata(record, source, state) {
271
282
  return;
272
283
  }
273
284
 
285
+ if (source === 'pi') {
286
+ if (record.type === 'session') {
287
+ state.sessionId = record.id || state.sessionId;
288
+ state.cwd = record.cwd || state.cwd;
289
+ }
290
+ return;
291
+ }
292
+
274
293
  if (!state.sessionId && record.sessionId) {
275
294
  state.sessionId = record.sessionId;
276
295
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -29,14 +29,14 @@ function shouldUseFastSessionBrowseLimit(options = {}) {
29
29
 
30
30
  export function isSessionQueryEnabled(source) {
31
31
  const normalized = normalizeSessionSource(source, '');
32
- return normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'all';
32
+ return normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'pi' || normalized === 'all';
33
33
  }
34
34
 
35
35
  export function normalizeSessionSource(source, fallback = 'all') {
36
36
  const normalized = typeof source === 'string'
37
37
  ? source.trim().toLowerCase()
38
38
  : '';
39
- if (normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'all') {
39
+ if (normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'pi' || normalized === 'all') {
40
40
  return normalized;
41
41
  }
42
42
  return fallback;
@@ -209,7 +209,7 @@ export function buildUsageHeatmap(sessions = [], options = {}) {
209
209
  for (const session of list) {
210
210
  if (!session || typeof session !== 'object') continue;
211
211
  const source = normalizeSessionSource(session.source, '');
212
- if (source !== 'codex' && source !== 'claude') continue;
212
+ if (source !== 'codex' && source !== 'claude' && source !== 'pi') continue;
213
213
  const updatedAtMs = Date.parse(session.updatedAt || '');
214
214
  if (!Number.isFinite(updatedAtMs)) continue;
215
215
  normalized.push({
@@ -309,7 +309,7 @@ export function buildUsageHourlyHeatmap(sessions = [], options = {}) {
309
309
  for (const session of list) {
310
310
  if (!session || typeof session !== 'object') continue;
311
311
  const source = normalizeSessionSource(session.source, '');
312
- if (source !== 'codex' && source !== 'claude') continue;
312
+ if (source !== 'codex' && source !== 'claude' && source !== 'pi') continue;
313
313
  const updatedAtMs = Date.parse(session.updatedAt || '');
314
314
  if (!Number.isFinite(updatedAtMs)) continue;
315
315
  const dayStart = toUtcDayStartMs(updatedAtMs);
@@ -402,7 +402,7 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
402
402
  for (const [sessionIndex, session] of list.entries()) {
403
403
  if (!session || typeof session !== 'object') continue;
404
404
  const source = normalizeSessionSource(session.source, '');
405
- if (source !== 'codex' && source !== 'claude') continue;
405
+ if (source !== 'codex' && source !== 'claude' && source !== 'pi') continue;
406
406
  const updatedAtMs = Date.parse(session.updatedAt || '');
407
407
  if (!Number.isFinite(updatedAtMs)) continue;
408
408
  const createdAtMs = Date.parse(session.createdAt || '');
@@ -307,7 +307,8 @@ export function createSessionComputed() {
307
307
  { value: "codex", label: this.t("sessions.source.codex") },
308
308
  { value: "claude", label: this.t("sessions.source.claudeCode") },
309
309
  { value: "gemini", label: this.t("sessions.source.gemini") },
310
- { value: "codebuddy", label: this.t("sessions.source.codebuddy") }
310
+ { value: "codebuddy", label: this.t("sessions.source.codebuddy") },
311
+ { value: "pi", label: this.t("sessions.source.pi") }
311
312
  ];
312
313
  },
313
314
  activeSessionExportKey() {
@@ -28,8 +28,8 @@ export function createSessionActionMethods(options = {}) {
28
28
  let error = '';
29
29
  if (!source) {
30
30
  error = '缺少 source 参数';
31
- } else if (source !== 'codex' && source !== 'claude') {
32
- error = 'source 仅支持 codex 或 claude';
31
+ } else if (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'pi') {
32
+ error = 'source 仅支持 codex、claude、gemini、codebuddy 或 pi';
33
33
  }
34
34
  if (!sessionId && !filePath) {
35
35
  error = error ? `${error},还缺少 sessionId 或 filePath` : '缺少 sessionId 或 filePath 参数';
@@ -67,7 +67,15 @@ export function createSessionActionMethods(options = {}) {
67
67
  return;
68
68
  }
69
69
 
70
- const sourceLabel = context.params.source === 'codex' ? 'Codex' : 'Claude Code';
70
+ const sourceLabel = context.params.source === 'codex'
71
+ ? 'Codex'
72
+ : (context.params.source === 'claude'
73
+ ? 'Claude Code'
74
+ : (context.params.source === 'gemini'
75
+ ? 'Gemini CLI'
76
+ : (context.params.source === 'pi'
77
+ ? 'Pi'
78
+ : (context.params.source === 'codebuddy' ? 'CodeBuddy Code' : context.params.source))));
71
79
  this.activeSession = {
72
80
  source: context.params.source,
73
81
  sourceLabel,
@@ -96,7 +104,7 @@ export function createSessionActionMethods(options = {}) {
96
104
  buildSessionStandaloneUrl(session) {
97
105
  if (!session) return '';
98
106
  const source = typeof session.source === 'string' ? session.source.trim().toLowerCase() : '';
99
- if (!source || (source !== 'codex' && source !== 'claude')) return '';
107
+ if (!source || (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'pi')) return '';
100
108
  const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
101
109
  const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
102
110
  if (!sessionId && !filePath) return '';
@@ -217,7 +225,7 @@ export function createSessionActionMethods(options = {}) {
217
225
  isDeleteAvailable(session) {
218
226
  if (!session) return false;
219
227
  const source = String(session.source || '').trim().toLowerCase();
220
- if (source !== 'codex' && source !== 'claude') return false;
228
+ if (source !== 'codex' && source !== 'claude' && source !== 'pi') return false;
221
229
  const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
222
230
  const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
223
231
  return !!sessionId || !!filePath;
@@ -120,7 +120,7 @@ export function createSessionBrowserMethods(options = {}) {
120
120
  syncSessionPathOptionsForSource(source, nextOptions, mergeWithExisting = false) {
121
121
  const targetSource = source === 'claude'
122
122
  ? 'claude'
123
- : (source === 'gemini' ? 'gemini' : (source === 'all' ? 'all' : 'codex'));
123
+ : (source === 'gemini' ? 'gemini' : (source === 'pi' ? 'pi' : (source === 'all' ? 'all' : 'codex')));
124
124
  const current = Array.isArray(this.sessionPathOptionsMap[targetSource])
125
125
  ? this.sessionPathOptionsMap[targetSource]
126
126
  : [];
@@ -137,7 +137,7 @@ export function createSessionBrowserMethods(options = {}) {
137
137
  refreshSessionPathOptions(source) {
138
138
  const targetSource = source === 'claude'
139
139
  ? 'claude'
140
- : (source === 'gemini' ? 'gemini' : (source === 'all' ? 'all' : 'codex'));
140
+ : (source === 'gemini' ? 'gemini' : (source === 'pi' ? 'pi' : (source === 'all' ? 'all' : 'codex')));
141
141
  const base = Array.isArray(this.sessionPathOptionsMap[targetSource])
142
142
  ? [...this.sessionPathOptionsMap[targetSource]]
143
143
  : [];
@@ -161,7 +161,7 @@ export function createSessionBrowserMethods(options = {}) {
161
161
  async loadSessionPathOptions(options = {}) {
162
162
  const source = options.source === 'claude'
163
163
  ? 'claude'
164
- : (options.source === 'gemini' ? 'gemini' : (options.source === 'all' ? 'all' : 'codex'));
164
+ : (options.source === 'gemini' ? 'gemini' : (options.source === 'pi' ? 'pi' : (options.source === 'all' ? 'all' : 'codex')));
165
165
  const forceRefresh = !!options.forceRefresh;
166
166
  const loaded = !!this.sessionPathOptionsLoadedMap[source];
167
167
  if (!forceRefresh && loaded) {
@@ -507,7 +507,11 @@ export function createSessionBrowserMethods(options = {}) {
507
507
  ? this.t('sessions.source.claudeCode')
508
508
  : (this.sessionFilterSource === 'gemini'
509
509
  ? this.t('sessions.source.gemini')
510
- : (this.sessionFilterSource === 'codebuddy' ? this.t('sessions.source.codebuddy') : this.sessionFilterSource)));
510
+ : (this.sessionFilterSource === 'codebuddy'
511
+ ? this.t('sessions.source.codebuddy')
512
+ : (this.sessionFilterSource === 'pi'
513
+ ? this.t('sessions.source.pi')
514
+ : this.sessionFilterSource))));
511
515
  chips.push({ key: 'source', title: this.t('sessions.filters.source'), value: label });
512
516
  }
513
517
  if (this.sessionPathFilter) {
@@ -10,7 +10,7 @@ export function createSessionTrashMethods(options = {}) {
10
10
  const deletedAt = typeof result.deletedAt === 'string' && result.deletedAt
11
11
  ? result.deletedAt
12
12
  : new Date().toISOString();
13
- const source = session && (session.source === 'claude' || session.source === 'gemini')
13
+ const source = session && (session.source === 'claude' || session.source === 'gemini' || session.source === 'codebuddy' || session.source === 'pi')
14
14
  ? session.source
15
15
  : 'codex';
16
16
  return {
@@ -18,7 +18,7 @@ export function createSessionTrashMethods(options = {}) {
18
18
  source,
19
19
  sourceLabel: session && typeof session.sourceLabel === 'string' && session.sourceLabel
20
20
  ? session.sourceLabel
21
- : (source === 'claude' ? 'Claude Code' : (source === 'gemini' ? 'Gemini CLI' : 'Codex')),
21
+ : (source === 'claude' ? 'Claude Code' : (source === 'gemini' ? 'Gemini CLI' : (source === 'pi' ? 'Pi' : (source === 'codebuddy' ? 'CodeBuddy Code' : 'Codex')))),
22
22
  sessionId: session && typeof session.sessionId === 'string' ? session.sessionId : '',
23
23
  title: session && typeof session.title === 'string' && session.title
24
24
  ? session.title
@@ -645,6 +645,7 @@ const en = Object.freeze({
645
645
  'sessions.source.claudeCode': 'Claude Code',
646
646
  'sessions.source.gemini': 'Gemini CLI',
647
647
  'sessions.source.codebuddy': 'CodeBuddy Code',
648
+ 'sessions.source.pi': 'Pi',
648
649
  'sessions.loadingList': 'Loading sessions...',
649
650
  'sessions.empty': 'No sessions found',
650
651
  'sessions.unknownTime': 'unknown time',
@@ -647,6 +647,7 @@ const ja = Object.freeze({
647
647
  'sessions.source.claudeCode': 'Claude Code',
648
648
  'sessions.source.gemini': 'Gemini',
649
649
  'sessions.source.codebuddy': 'CodeBuddy',
650
+ 'sessions.source.pi': 'Pi',
650
651
  'sessions.loadingList': 'セッション一覧を読み込み中...',
651
652
  'sessions.empty': 'セッションがありません',
652
653
  'sessions.unknownTime': '不明な時間',
@@ -850,6 +850,7 @@ const vi = Object.freeze({
850
850
  'sessions.source.claudeCode': 'Claude Code',
851
851
  'sessions.source.gemini': 'Gemini CLI',
852
852
  'sessions.source.codebuddy': 'CodeBuddy Code',
853
+ 'sessions.source.pi': 'Pi',
853
854
  'sessions.loadingList': 'Đang tải phiên làm việc...',
854
855
  'sessions.empty': 'Không tìm thấy phiên nào',
855
856
  'sessions.unknownTime': 'thời gian không rõ',
@@ -645,6 +645,7 @@ const zhTw = Object.freeze({
645
645
  'sessions.source.claudeCode': 'Claude Code',
646
646
  'sessions.source.gemini': 'Gemini CLI',
647
647
  'sessions.source.codebuddy': 'CodeBuddy Code',
648
+ 'sessions.source.pi': 'Pi',
648
649
  'sessions.loadingList': '會話載入中...',
649
650
  'sessions.empty': '暫無可用會話記錄',
650
651
  'sessions.unknownTime': '未知時間',
@@ -645,6 +645,7 @@ const zh = Object.freeze({
645
645
  'sessions.source.claudeCode': 'Claude Code',
646
646
  'sessions.source.gemini': 'Gemini CLI',
647
647
  'sessions.source.codebuddy': 'CodeBuddy Code',
648
+ 'sessions.source.pi': 'Pi',
648
649
  'sessions.loadingList': '会话加载中...',
649
650
  'sessions.empty': '暂无可用会话记录',
650
651
  'sessions.unknownTime': '未知时间',
@@ -220,35 +220,30 @@
220
220
  </div>
221
221
  </div>
222
222
  <div class="session-actions">
223
- <button class="btn-session-refresh" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession"
223
+ <button class="btn-session-refresh has-label" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession"
224
224
  :title="sessionDetailLoading ? t('sessions.preview.loading') : t('sessions.preview.refresh')"
225
225
  :aria-label="sessionDetailLoading ? t('sessions.preview.loading') : t('sessions.preview.refresh')">
226
226
  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 8a5.5 5.5 0 0 1 9.4-3.8"/><path d="M13.5 8a5.5 5.5 0 0 1-9.4 3.8"/><polyline points="2.5 2 2.5 5 5.5 5"/><polyline points="13.5 14 13.5 11 10.5 11"/></svg>
227
+ <span class="session-action-label">{{ t('sessions.preview.refresh') }}</span>
227
228
  </button>
228
229
  <button
229
230
  v-if="isDeleteAvailable(activeSession)"
230
- class="btn-session-delete"
231
+ class="btn-session-delete has-label"
231
232
  @click="deleteSession(activeSession)"
232
233
  :disabled="!activeSession || sessionsLoading || sessionDeleting[getSessionExportKey(activeSession)]"
233
234
  :title="(activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? t('sessions.preview.deleting') : t('sessions.preview.moving')) : (sessionTrashEnabled === false ? t('sessions.preview.deleteHard') : t('sessions.preview.moveToTrash'))"
234
235
  :aria-label="(activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? t('sessions.preview.deleting') : t('sessions.preview.moving')) : (sessionTrashEnabled === false ? t('sessions.preview.deleteHard') : t('sessions.preview.moveToTrash'))">
235
236
  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 4 4 4 13 4"/><path d="M5.5 4V2.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V4"/><path d="M12 4v9.5a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 4 13.5V4"/></svg>
237
+ <span class="session-action-label">{{ (activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? t('sessions.preview.deleting') : t('sessions.preview.moving')) : (sessionTrashEnabled === false ? t('sessions.preview.deleteHard') : t('sessions.preview.moveToTrash')) }}</span>
236
238
  </button>
237
239
  <button
238
- class="btn-session-export"
240
+ class="btn-session-export has-label"
239
241
  @click="exportSession(activeSession)"
240
242
  :disabled="!activeSession || sessionExporting[getSessionExportKey(activeSession)]"
241
243
  :title="(activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? t('sessions.preview.exporting') : t('sessions.preview.export')"
242
244
  :aria-label="(activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? t('sessions.preview.exporting') : t('sessions.preview.export')">
243
245
  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v8"/><polyline points="4 7 8 10.5 12 7"/><path d="M2.5 12v1.5a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V12"/></svg>
244
- </button>
245
- <button
246
- class="btn-session-open session-brief-copy"
247
- @click="copySessionWorkspaceBrief"
248
- :disabled="!activeSessionWorkspaceSummary.available"
249
- :title="t('sessions.workspace.copy')"
250
- :aria-label="t('sessions.workspace.copy')">
251
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2.5h6.5L13 5v8.5H4z"/><path d="M10.5 2.5V5H13"/><path d="M6 8h5"/><path d="M6 10.5h5"/></svg>
246
+ <span class="session-action-label">{{ (activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? t('sessions.preview.exporting') : t('sessions.preview.export') }}</span>
252
247
  </button>
253
248
  <div class="session-link-group">
254
249
  <button
@@ -269,12 +264,13 @@
269
264
  </button>
270
265
  </div>
271
266
  <button
272
- class="btn-session-open"
267
+ class="btn-session-open has-label"
273
268
  @click="copySessionPath(activeSession)"
274
269
  :disabled="!activeSession || !getSessionFilePath(activeSession)"
275
270
  :title="t('sessions.preview.copyPath')"
276
271
  :aria-label="t('sessions.preview.copyPath')">
277
272
  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 5.5V12.5a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5a1 1 0 0 0-1-1H8L6.5 5H3a1 1 0 0 0-1 1z"/></svg>
273
+ <span class="session-action-label">{{ t('sessions.preview.copyPath') }}</span>
278
274
  </button>
279
275
  </div>
280
276
  </div>
@@ -3512,7 +3512,7 @@ return function render(_ctx, _cache) {
3512
3512
  ]),
3513
3513
  _createElementVNode("div", { class: "session-actions" }, [
3514
3514
  _createElementVNode("button", {
3515
- class: "btn-session-refresh",
3515
+ class: "btn-session-refresh has-label",
3516
3516
  onClick: _ctx.loadActiveSessionDetail,
3517
3517
  disabled: _ctx.sessionDetailLoading || !_ctx.activeSession,
3518
3518
  title: _ctx.sessionDetailLoading ? _ctx.t('sessions.preview.loading') : _ctx.t('sessions.preview.refresh'),
@@ -3530,12 +3530,13 @@ return function render(_ctx, _cache) {
3530
3530
  _createElementVNode("path", { d: "M13.5 8a5.5 5.5 0 0 1-9.4 3.8" }),
3531
3531
  _createElementVNode("polyline", { points: "2.5 2 2.5 5 5.5 5" }),
3532
3532
  _createElementVNode("polyline", { points: "13.5 14 13.5 11 10.5 11" })
3533
- ]))
3533
+ ])),
3534
+ _createElementVNode("span", { class: "session-action-label" }, _toDisplayString(_ctx.t('sessions.preview.refresh')), 1 /* TEXT */)
3534
3535
  ], 8 /* PROPS */, ["onClick", "disabled", "title", "aria-label"]),
3535
3536
  (_ctx.isDeleteAvailable(_ctx.activeSession))
3536
3537
  ? (_openBlock(), _createElementBlock("button", {
3537
3538
  key: 0,
3538
- class: "btn-session-delete",
3539
+ class: "btn-session-delete has-label",
3539
3540
  onClick: $event => (_ctx.deleteSession(_ctx.activeSession)),
3540
3541
  disabled: !_ctx.activeSession || _ctx.sessionsLoading || _ctx.sessionDeleting[_ctx.getSessionExportKey(_ctx.activeSession)],
3541
3542
  title: (_ctx.activeSession && _ctx.sessionDeleting[_ctx.getSessionExportKey(_ctx.activeSession)]) ? (_ctx.sessionTrashEnabled === false ? _ctx.t('sessions.preview.deleting') : _ctx.t('sessions.preview.moving')) : (_ctx.sessionTrashEnabled === false ? _ctx.t('sessions.preview.deleteHard') : _ctx.t('sessions.preview.moveToTrash')),
@@ -3552,11 +3553,12 @@ return function render(_ctx, _cache) {
3552
3553
  _createElementVNode("polyline", { points: "3 4 4 4 13 4" }),
3553
3554
  _createElementVNode("path", { d: "M5.5 4V2.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V4" }),
3554
3555
  _createElementVNode("path", { d: "M12 4v9.5a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 4 13.5V4" })
3555
- ]))
3556
+ ])),
3557
+ _createElementVNode("span", { class: "session-action-label" }, _toDisplayString((_ctx.activeSession && _ctx.sessionDeleting[_ctx.getSessionExportKey(_ctx.activeSession)]) ? (_ctx.sessionTrashEnabled === false ? _ctx.t('sessions.preview.deleting') : _ctx.t('sessions.preview.moving')) : (_ctx.sessionTrashEnabled === false ? _ctx.t('sessions.preview.deleteHard') : _ctx.t('sessions.preview.moveToTrash'))), 1 /* TEXT */)
3556
3558
  ], 8 /* PROPS */, ["onClick", "disabled", "title", "aria-label"]))
3557
3559
  : _createCommentVNode("v-if", true),
3558
3560
  _createElementVNode("button", {
3559
- class: "btn-session-export",
3561
+ class: "btn-session-export has-label",
3560
3562
  onClick: $event => (_ctx.exportSession(_ctx.activeSession)),
3561
3563
  disabled: !_ctx.activeSession || _ctx.sessionExporting[_ctx.getSessionExportKey(_ctx.activeSession)],
3562
3564
  title: (_ctx.activeSession && _ctx.sessionExporting[_ctx.getSessionExportKey(_ctx.activeSession)]) ? _ctx.t('sessions.preview.exporting') : _ctx.t('sessions.preview.export'),
@@ -3573,28 +3575,8 @@ return function render(_ctx, _cache) {
3573
3575
  _createElementVNode("path", { d: "M8 2v8" }),
3574
3576
  _createElementVNode("polyline", { points: "4 7 8 10.5 12 7" }),
3575
3577
  _createElementVNode("path", { d: "M2.5 12v1.5a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V12" })
3576
- ]))
3577
- ], 8 /* PROPS */, ["onClick", "disabled", "title", "aria-label"]),
3578
- _createElementVNode("button", {
3579
- class: "btn-session-open session-brief-copy",
3580
- onClick: _ctx.copySessionWorkspaceBrief,
3581
- disabled: !_ctx.activeSessionWorkspaceSummary.available,
3582
- title: _ctx.t('sessions.workspace.copy'),
3583
- "aria-label": _ctx.t('sessions.workspace.copy')
3584
- }, [
3585
- (_openBlock(), _createElementBlock("svg", {
3586
- viewBox: "0 0 16 16",
3587
- fill: "none",
3588
- stroke: "currentColor",
3589
- "stroke-width": "1.8",
3590
- "stroke-linecap": "round",
3591
- "stroke-linejoin": "round"
3592
- }, [
3593
- _createElementVNode("path", { d: "M4 2.5h6.5L13 5v8.5H4z" }),
3594
- _createElementVNode("path", { d: "M10.5 2.5V5H13" }),
3595
- _createElementVNode("path", { d: "M6 8h5" }),
3596
- _createElementVNode("path", { d: "M6 10.5h5" })
3597
- ]))
3578
+ ])),
3579
+ _createElementVNode("span", { class: "session-action-label" }, _toDisplayString((_ctx.activeSession && _ctx.sessionExporting[_ctx.getSessionExportKey(_ctx.activeSession)]) ? _ctx.t('sessions.preview.exporting') : _ctx.t('sessions.preview.export')), 1 /* TEXT */)
3598
3580
  ], 8 /* PROPS */, ["onClick", "disabled", "title", "aria-label"]),
3599
3581
  _createElementVNode("div", { class: "session-link-group" }, [
3600
3582
  _createElementVNode("button", {
@@ -3638,7 +3620,7 @@ return function render(_ctx, _cache) {
3638
3620
  ], 8 /* PROPS */, ["onClick", "disabled", "title", "aria-label"])
3639
3621
  ]),
3640
3622
  _createElementVNode("button", {
3641
- class: "btn-session-open",
3623
+ class: "btn-session-open has-label",
3642
3624
  onClick: $event => (_ctx.copySessionPath(_ctx.activeSession)),
3643
3625
  disabled: !_ctx.activeSession || !_ctx.getSessionFilePath(_ctx.activeSession),
3644
3626
  title: _ctx.t('sessions.preview.copyPath'),
@@ -3653,7 +3635,8 @@ return function render(_ctx, _cache) {
3653
3635
  "stroke-linejoin": "round"
3654
3636
  }, [
3655
3637
  _createElementVNode("path", { d: "M2 5.5V12.5a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5a1 1 0 0 0-1-1H8L6.5 5H3a1 1 0 0 0-1 1z" })
3656
- ]))
3638
+ ])),
3639
+ _createElementVNode("span", { class: "session-action-label" }, _toDisplayString(_ctx.t('sessions.preview.copyPath')), 1 /* TEXT */)
3657
3640
  ], 8 /* PROPS */, ["onClick", "disabled", "title", "aria-label"])
3658
3641
  ])
3659
3642
  ], 512 /* NEED_PATCH */),
@@ -366,6 +366,14 @@ textarea:focus-visible {
366
366
  height: 44px;
367
367
  }
368
368
 
369
+ .btn-session-export.has-label,
370
+ .btn-session-open.has-label,
371
+ .btn-session-refresh.has-label,
372
+ .btn-session-delete.has-label {
373
+ width: auto;
374
+ height: 44px;
375
+ padding: 0 14px;
376
+ }
369
377
  .session-link-group .btn-session-open {
370
378
  width: 44px;
371
379
  height: 44px;
@@ -585,6 +593,10 @@ textarea:focus-visible {
585
593
  height: 40px;
586
594
  }
587
595
 
596
+ .session-actions .session-action-label {
597
+ display: none;
598
+ }
599
+
588
600
  .session-preview-body {
589
601
  padding: 12px;
590
602
  }
@@ -544,12 +544,34 @@
544
544
  transform: none;
545
545
  }
546
546
 
547
+ .btn-session-export.has-label,
548
+ .btn-session-open.has-label,
549
+ .btn-session-clone.has-label,
550
+ .btn-session-refresh.has-label,
551
+ .btn-session-delete.has-label {
552
+ width: auto;
553
+ min-width: 32px;
554
+ height: 32px;
555
+ padding: 0 var(--spacing-sm);
556
+ gap: 6px;
557
+ font-size: var(--font-size-caption);
558
+ font-weight: var(--font-weight-secondary);
559
+ line-height: 1;
560
+ white-space: nowrap;
561
+ }
562
+
563
+ .session-action-label {
564
+ font-size: inherit;
565
+ line-height: 1;
566
+ letter-spacing: 0.01em;
567
+ white-space: nowrap;
568
+ }
569
+
547
570
  .btn-icon-sm {
548
571
  width: 16px;
549
572
  height: 16px;
550
573
  flex-shrink: 0;
551
574
  }
552
-
553
575
  /* ===== Session match badges ===== */
554
576
  .session-match-badge {
555
577
  display: inline-flex;