feat: live Repo/CI panel, env.local overrides (0.3.0)
build-deb / Lint, test, package (push) Successful in 1m46s
build-deb / Notify on failure (push) Skipped

- `devdesk attach --repo NAME` / `devdesk repo NAME`: git state of the session's
  folder (branch, ahead/behind, uncommitted changes) plus the Gitea commit
  status of HEAD and open PR count. Uses the anonymous API for public repos,
  or /etc/devdesk/gitea-token when present. Runs over the ttyd channel, so no
  sidecar is needed. The panel links to the repo, the CI run and PRs, and a
  notification fires when HEAD's CI finishes while the tab is in the background.
- /etc/devdesk/env.local is read after the packaged env and never shipped, so
  upgrades no longer stop at a conffile prompt.
- Hooks and status line only record for panes on DevDesk's own tmux server; the
  Claude wrapper re-reads env before every launch (so the Remote Control toggle
  applies on the next Claude restart).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 13:09:32 -04:00
co-authored by Claude Opus 5.5
parent ae8b5d843f
commit 9026238024
15 changed files with 256 additions and 20 deletions
+35 -2
View File
@@ -1,9 +1,10 @@
/* global __DEVDESK_VERSION__ */
import { createTerminal, loadFonts } from './term.js';
import { TtydConnection, fetchToken, AuthExpiredError, UnauthorizedError } from './connection.js';
import { TtydConnection, fetchRepo, fetchToken, AuthExpiredError, UnauthorizedError } from './connection.js';
import { parseTitle, sessionFromSearch } from './protocol.js';
import { SessionsPanel, rememberSession } from './sessions.js';
import { renderContext } from './context.js';
import { renderRepo } from './repo.js';
import { bindAskClaude } from './widgets.js';
import * as notify from './notify.js';
@@ -96,6 +97,34 @@ function showContext() {
renderContext($('widget-context'), state.session, current?.context ?? null);
}
// Repo / CI panel for the current session; notifies when HEAD's CI finishes.
let lastCi = { key: '', state: '' };
let repoBusy = false;
async function refreshRepo() {
const session = state.session;
if (!session) {
renderRepo($('widget-repo'), null);
return;
}
if (repoBusy) return;
repoBusy = true;
try {
const repo = await fetchRepo(session);
if (session !== state.session) return;
renderRepo($('widget-repo'), repo);
const key = `${session}@${repo.sha}`;
const ci = repo.ci?.total ? repo.ci.state : '';
if (key === lastCi.key && lastCi.state === 'pending' && ['success', 'failure', 'error'].includes(ci)) {
notify.ring(session, `CI ${ci === 'success' ? 'passed' : 'failed'} on ${repo.branch} @ ${repo.sha}`);
}
lastCi = { key, state: ci };
} catch {
// Keep the last rendering; the next refresh retries.
} finally {
repoBusy = false;
}
}
let refreshTimer = 0;
function refreshSoon(delay = 1500) {
clearTimeout(refreshTimer);
@@ -133,6 +162,7 @@ function connect(session) {
setWsState('connected', 'live');
setSso('ok');
if (session) rememberSession(session);
refreshRepo();
fit.fit();
term.focus();
},
@@ -394,7 +424,10 @@ async function boot() {
ui.sessionChip.textContent = to;
sessionsPanel.setCurrent(to);
},
onData: () => showContext(),
onData: () => {
showContext();
refreshRepo();
},
},
});
bindAskClaude($('sidebar'), (text) => {