claude-home 1.4.0 → 1.4.4

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": "claude-home",
3
- "version": "1.4.0",
3
+ "version": "1.4.4",
4
4
  "description": "Web dashboard for Claude Code — browse sessions, manage skills, hooks, commands, and agents",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -343,6 +343,9 @@
343
343
 
344
344
  .session-resume-btn:hover { background: var(--red); }
345
345
  .session-resume-btn.copied { background: var(--green) !important; }
346
+ .session-resume-btn:disabled { opacity: 0.35; cursor: not-allowed; background: var(--surface-2); color: var(--ink-3); }
347
+ .session-resume-btn:disabled:hover { background: var(--surface-2); }
348
+ .session-row:hover .session-resume-btn:disabled { opacity: 0.4; }
346
349
 
347
350
  /* ── Chat view ────────────────────────────────────────── */
348
351
  .chat-header {
@@ -1700,7 +1703,7 @@
1700
1703
  </template>
1701
1704
  <span x-text="formatDate(sessions[0].modified)"></span>
1702
1705
  <span x-text="sessions[0].messageCount + ' msgs'"></span>
1703
- <button class="btn btn-primary btn-sm" style="margin-left:auto" :id="'resume-db'" @click.stop="resumeSession(sessions[0].sessionId,'resume-db')">Resume →</button>
1706
+ <button class="btn btn-primary btn-sm" style="margin-left:auto" :id="'resume-db'" :disabled="!sessions[0].resumable" :title="!sessions[0].resumable ? 'Archivo de sesión eliminado — no se puede retomar' : 'Copiar comando para retomar esta sesión'" @click.stop="sessions[0].resumable && resumeSession(sessions[0].sessionId,'resume-db', sessions[0].projectPath)">Resume →</button>
1704
1707
  </div>
1705
1708
  </div>
1706
1709
  </template>
@@ -1864,7 +1867,7 @@
1864
1867
  </div>
1865
1868
  </div>
1866
1869
  <div class="session-right">
1867
- <button class="session-resume-btn" :id="'resume-' + s.sessionId" @click.stop="resumeSession(s.sessionId, 'resume-' + s.sessionId)">Resume →</button>
1870
+ <button class="session-resume-btn" :id="'resume-' + s.sessionId" :disabled="!s.resumable" :title="!s.resumable ? 'Archivo de sesión eliminado — no se puede retomar' : 'Copiar comando para retomar esta sesión'" @click.stop="s.resumable && resumeSession(s.sessionId, 'resume-' + s.sessionId, s.projectPath)">Resume →</button>
1868
1871
  </div>
1869
1872
  </div>
1870
1873
  </template>
@@ -1981,7 +1984,7 @@
1981
1984
  </div>
1982
1985
  <button class="btn btn-sm" style="background:var(--red-dim,#3a1a1a);color:var(--red);border:1px solid var(--red)" @click="deleteSession()" x-show="!deletingSession">Delete</button>
1983
1986
  <span x-show="deletingSession" style="font-size:12px;color:var(--ink-3)">Deleting…</span>
1984
- <button class="btn btn-primary btn-sm" id="resume-detail" @click="resumeSession(selectedSession.sessionId, 'resume-detail')">
1987
+ <button class="btn btn-primary btn-sm" id="resume-detail" :disabled="!sessionDetail?.resumable" :title="!sessionDetail?.resumable ? 'Archivo de sesión eliminado — no se puede retomar' : 'Copiar comando para retomar esta sesión'" @click="sessionDetail?.resumable && resumeSession(selectedSession.sessionId, 'resume-detail', selectedSession.projectPath)">
1985
1988
  Resume →
1986
1989
  </button>
1987
1990
  <span x-show="exportMsg" x-text="exportMsg" style="font-size:12px;color:var(--green)" x-transition></span>
@@ -3638,8 +3641,10 @@
3638
3641
  <input class="perm-add-input" style="width:100%" x-model="ruleBuilder.specifier"
3639
3642
  placeholder="npm run * / git commit * / * --help *"
3640
3643
  @keydown.enter="ruleBuilderAdd()" />
3641
- <div style="font-size:11px;color:var(--ink-3);margin-top:5px">
3642
- <code>Bash(ls *)</code> coincide con <code>ls -la</code> pero no con <code>lsof</code> — el espacio antes de <code>*</code> actúa como límite de palabra.
3644
+ <div style="font-size:11px;color:var(--ink-3);margin-top:5px;display:flex;flex-direction:column;gap:4px">
3645
+ <span><code>Bash(ls *)</code> coincide con <code>ls -la</code> pero no con <code>lsof</code> — el espacio antes de <code>*</code> actúa como límite de palabra.</span>
3646
+ <span>⚠️ Comandos con <code>&&</code>, <code>|</code> o redirecciones pueden ser bloqueados por la detección automática de patrones peligrosos, aunque estén en allow.</span>
3647
+ <span>💡 Para comandos de una sola herramienta usa el prefijo sin encadenar: <code>npm publish *</code> en lugar de <code>cd /dir && npm publish</code>.</span>
3643
3648
  </div>
3644
3649
  </div>
3645
3650
  </template>
@@ -4006,15 +4011,27 @@
4006
4011
  async init() {
4007
4012
  const savedView = localStorage.getItem('cs:view');
4008
4013
  const savedProject = localStorage.getItem('cs:project');
4009
- if (savedView) this.view = savedView;
4010
- if (savedProject) this.filterProject = savedProject;
4011
-
4012
- this.$watch('view', v => localStorage.setItem('cs:view', v));
4013
- this.$watch('filterProject', v => localStorage.setItem('cs:project', v));
4014
+ const savedBranch = localStorage.getItem('cs:branch');
4015
+ const savedText = localStorage.getItem('cs:filterText');
4016
+ const savedFrom = localStorage.getItem('cs:filterFrom');
4017
+ const savedTo = localStorage.getItem('cs:filterTo');
4018
+ if (savedView) this.view = savedView;
4019
+ if (savedText) this.filterText = savedText;
4020
+ if (savedFrom) this.filterFrom = savedFrom;
4021
+ if (savedTo) this.filterTo = savedTo;
4022
+
4023
+ this.$watch('view', v => localStorage.setItem('cs:view', v));
4024
+ this.$watch('filterProject', v => localStorage.setItem('cs:project', v));
4025
+ this.$watch('filterBranch', v => localStorage.setItem('cs:branch', v));
4026
+ this.$watch('filterText', v => localStorage.setItem('cs:filterText', v));
4027
+ this.$watch('filterFrom', v => localStorage.setItem('cs:filterFrom', v));
4028
+ this.$watch('filterTo', v => localStorage.setItem('cs:filterTo', v));
4014
4029
  this.$watch('configProject', () => { this.config = null; this.loadConfig(); this.hooksLog = null; this.toolCommands = []; this.toolSkills = []; this.agents = []; if (this.view === 'commands' || this.view === 'skills') this.loadTools(); if (this.view === 'agents') this.loadAgents(); });
4015
4030
 
4016
4031
  await this.loadProjects();
4032
+ if (savedProject) this.filterProject = savedProject;
4017
4033
  await this.loadBranches();
4034
+ if (savedBranch) this.filterBranch = savedBranch;
4018
4035
  await this.loadSessions();
4019
4036
  this.initView(this.view);
4020
4037
  this.loadCosts();
@@ -4072,15 +4089,21 @@
4072
4089
  await this.openSession(s);
4073
4090
  },
4074
4091
 
4075
- async resumeSession(sessionId, btnId) {
4076
- const cmd = `claude -r ${sessionId}`;
4077
- await navigator.clipboard.writeText(cmd);
4092
+ async resumeSession(sessionId, btnId, projectPath) {
4093
+ const cmd = projectPath
4094
+ ? `cd "${projectPath}" && claude -r ${sessionId}`
4095
+ : `claude -r ${sessionId}`;
4078
4096
  const btn = document.getElementById(btnId);
4079
- if (btn) {
4080
- const orig = btn.textContent;
4081
- btn.textContent = 'Copied!';
4082
- btn.classList.add('btn-copied', 'copied');
4083
- setTimeout(() => { btn.textContent = orig; btn.classList.remove('btn-copied', 'copied'); }, 2000);
4097
+ try {
4098
+ await navigator.clipboard.writeText(cmd);
4099
+ if (btn) {
4100
+ const orig = btn.textContent;
4101
+ btn.textContent = 'Copied!';
4102
+ btn.classList.add('btn-copied', 'copied');
4103
+ setTimeout(() => { btn.textContent = orig; btn.classList.remove('btn-copied', 'copied'); }, 2000);
4104
+ }
4105
+ } catch {
4106
+ prompt('Copia este comando:', cmd);
4084
4107
  }
4085
4108
  },
4086
4109
 
package/server.js CHANGED
@@ -272,7 +272,13 @@ async function loadSessionIndex(dirName) {
272
272
  };
273
273
  }));
274
274
 
275
- const allEntries = [...indexedEntries, ...unindexedEntries];
275
+ const allEntries = [
276
+ ...indexedEntries.map(e => {
277
+ const exists = fs.existsSync(e.fullPath || path.join(dir, `${e.sessionId}.jsonl`));
278
+ return { ...e, orphaned: !exists, resumable: exists };
279
+ }),
280
+ ...unindexedEntries.map(e => ({ ...e, resumable: true })),
281
+ ];
276
282
  indexCache.set(dirName, { indexMtime, dirMtime: dirStat, entries: allEntries });
277
283
  return allEntries;
278
284
  }
@@ -438,6 +444,7 @@ app.get('/api/sessions/:project/:sessionId', async (req, res) => {
438
444
  const { project, sessionId } = req.params;
439
445
  const filePath = path.join(PROJECTS_DIR, project, `${sessionId}.jsonl`);
440
446
  try {
447
+ const resumable = fs.existsSync(filePath);
441
448
  const messages = await parseJsonl(filePath);
442
449
  const tokens = aggregateTokens(messages);
443
450
  const models = [...new Set(
@@ -449,7 +456,7 @@ app.get('/api/sessions/:project/:sessionId', async (req, res) => {
449
456
  const cost = calculateCost(tokens, primaryModel);
450
457
  const savings = cacheSavings(tokens, primaryModel);
451
458
  const carbon = calculateCarbon(tokens, primaryModel);
452
- res.json({ sessionId, tokens, models, cost, savings, carbon, messages });
459
+ res.json({ sessionId, tokens, models, cost, savings, carbon, messages, resumable });
453
460
  } catch (e) {
454
461
  res.status(500).json({ error: e.message });
455
462
  }