mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix: apply the review items for #714 at merge
- Session update no longer resets "How often" while typing: updateGitFetchStatus (session-ui.js) leaves a control alone while it is document.activeElement, and autoSaveGitFetch writes the clamped interval back itself (Enter saves with the field still focused). - Partial override shows the app values: enabled = session ?? app switch, interval = session ?? app interval ?? 300 (session-ui.js updateGitFetchStatus). - runFetch (git-fetch.ts) arms the spawn with what the core.sshCommand read left of timeoutMs, and answers ETIMEDOUT without spawning when nothing is left, so a git that never answers costs one timeout, not two. - fetchEnvFor keeps a GIT_SSH from the server's environment (repoSshCommand || base.GIT_SSH), fileoverview and CLAUDE.md line updated to say so. - i18n.js: the zh-CN "Git fetch on an interval" description scopes the only writing git command to the Git window. - styles.css: the disabled/busy rules are scoped to .git-status-actions .btn-icon-sm (moved beside .git-status-actions), and the busy pulse stops under prefers-reduced-motion. - Comments that called the override per case now say per session: session.ts (field, restore and getter docs, setGitFetchSettings), git-fetch-scheduler.ts:2, server-timing.ts:78, server.ts (cleanup and restore), app.js _onSessionUpdated, session-ui.js (openSessionOptions and updateGitFetchStatus). - Tests: test/git-fetch-session-options-ui.test.ts (new, JSDOM: typing survives an SSE update, the Enter write-back, both partial cases); test/git-fetch.test.ts (GIT_SSH kept; one bound for the read and the fetch, stub git on PATH). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3411d7f35a
commit
6539bc6c82
12 files changed
+299
-68
No files matched your search
@@ -2455,8 +2455,9 @@ class CodemanApp {
|
||||
// Update parentSessionName for any subagents belonging to this session
|
||||
// (fixes stale name display after session rename)
|
||||
this.updateSubagentParentNames(session.id);
|
||||
// A scheduled fetch publishes the case's result on this same event, so an open Session Options modal on
|
||||
// that case shows it without polling its own (session-ui.js: updateGitFetchStatus).
|
||||
// A scheduled fetch publishes the session's result on this same event, so an open Session Options modal
|
||||
// on that session shows it without polling its own (session-ui.js: updateGitFetchStatus, which leaves the
|
||||
// control being typed in alone).
|
||||
if (this.editingSessionId === session.id) this.updateGitFetchStatus(session.id);
|
||||
// If claudeSessionId was just set, re-check orphan subagents
|
||||
// This connects subagents that were waiting for the session to identify itself
|
||||
|
||||
@@ -538,7 +538,7 @@
|
||||
'单条 git 命令可运行的秒数,超时后该仓库会被报告为无法读取(5 到 120,默认 30)。仓库位于较慢的网络共享上时请调高此值。',
|
||||
'Git fetch on an interval': 'Git 定时拉取',
|
||||
'Runs `git fetch` for the repositories the Git window lists on a timer, so the "behind" number is current without a click. Off by default: this is the only git command the Git panel runs that writes, and the only one Codeman runs on a timer (cloning and worktree management write too, but only when you asked for them at the moment you asked), so nothing runs until you turn it on. A session can override both the switch and the interval in Session Options → Git.':
|
||||
'按间隔为 Git 窗口列出的仓库运行拉取,这样“落后”的数字无需点击就是最新的。默认关闭:这是 {name} 运行的唯一会写入的 git 命令,也是唯一会按间隔自行运行的命令(克隆和工作树管理也会写入,但只在您当时要求时才运行),所以在您开启之前不会有任何运行。会话可以在“会话选项 → Git”中同时覆盖开关和间隔。',
|
||||
'按间隔为 Git 窗口列出的仓库运行拉取,这样“落后”的数字无需点击就是最新的。默认关闭:这是 Git 窗口运行的唯一会写入的 git 命令,也是 {name} 唯一会按间隔自行运行的命令(克隆和工作树管理也会写入,但只在您当时要求时才运行),所以在您开启之前不会有任何运行。会话可以在“会话选项 → Git”中同时覆盖开关和间隔。',
|
||||
'Git fetch: how often': 'Git 定时拉取:多久一次',
|
||||
'Seconds between runs for a session that has no override of its own (30 to 3600, default 300). Only a session that is on — here, or in Session Options → Git — is fetched, and sessions sharing one folder are fetched by one run.':
|
||||
'没有自己覆盖设置的会话,两次运行之间的秒数(30 到 3600,默认 300)。只有已开启的会话——这里,或在“会话选项 → Git”——才会被拉取,共用一个文件夹的会话由一次运行拉取。',
|
||||
|
||||
@@ -2685,7 +2685,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.getElementById('modalKeepWarmEnabled').checked =
|
||||
session.keepWarm?.enabled ?? session.keepWarmEnabled ?? false;
|
||||
this.updateKeepWarmStatus(sessionId);
|
||||
// Git: the case's own choice if it has one, otherwise the app setting. updateGitFetchStatus is also
|
||||
// Git: the session's own choice if it has one, otherwise the app setting. updateGitFetchStatus is also
|
||||
// what SSE calls when a scheduled run lands, so the rows stay current while the modal is open.
|
||||
this.updateGitFetchStatus(sessionId);
|
||||
document.getElementById('modalImageWatcherEnabled').checked = session.imageWatcherEnabled ?? true;
|
||||
@@ -3032,11 +3032,15 @@ Object.assign(CodemanApp.prototype, {
|
||||
},
|
||||
|
||||
/**
|
||||
* The Git tab: the case's last scheduled run, and which values its rows show. A case with no choice of its
|
||||
* own follows the app setting, so its switch and interval display the app's numbers and go inert — the same
|
||||
* inert-row pattern as updateKeepWarmStatus above — because changing them here is what makes the case stop
|
||||
* following. The app numbers come from the settings cache, so what this shows can be one settings load
|
||||
* The Git tab: the session's last scheduled run, and which values its rows show. A session with no choice of
|
||||
* its own follows the app setting, so its switch and interval display the app's numbers and go inert (the
|
||||
* same inert-row pattern as updateKeepWarmStatus above), because changing them here is what makes the session
|
||||
* stop following. The app numbers come from the settings cache, so what this shows can be one settings load
|
||||
* behind; the server decides from a fresh read, not from what the page displays.
|
||||
*
|
||||
* SSE calls this on every `session:updated` for the open session (app.js `_onSessionUpdated`), and a working
|
||||
* session sends one on each working/idle change, so a control the user is in (`document.activeElement`) is
|
||||
* left as it is: rewriting it put a half-typed interval back to the stored one before `onchange` could save it.
|
||||
*/
|
||||
updateGitFetchStatus(sessionId) {
|
||||
const statusEl = document.getElementById('gitFetchStatus');
|
||||
@@ -3048,15 +3052,17 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (!session) return;
|
||||
|
||||
const app = this.loadAppSettingsFromStorage() || {};
|
||||
// No choice of its own on either field means the session follows the app. A partial one — its own switch,
|
||||
// the app's interval — means it does not, and the row that still follows shows the app's value.
|
||||
// No choice of its own on either field means the session follows the app. A partial one (its own switch,
|
||||
// the app's interval) means it does not, and the row that still follows shows the app's value, which is
|
||||
// also the value the scheduler runs it with (a default here would show one number and fetch at another).
|
||||
const follows = session.gitFetchEnabled === undefined && session.gitFetchIntervalSeconds === undefined;
|
||||
const enabled = follows ? app.gitFetchEnabled === true : session.gitFetchEnabled ?? false;
|
||||
const interval = follows ? app.gitFetchIntervalSeconds ?? 300 : session.gitFetchIntervalSeconds ?? 300;
|
||||
const enabled = session.gitFetchEnabled ?? (app.gitFetchEnabled === true);
|
||||
const interval = session.gitFetchIntervalSeconds ?? app.gitFetchIntervalSeconds ?? 300;
|
||||
|
||||
followBox.checked = follows;
|
||||
box.checked = enabled;
|
||||
num.value = interval;
|
||||
const focused = document.activeElement;
|
||||
if (followBox !== focused) followBox.checked = follows;
|
||||
if (box !== focused) box.checked = enabled;
|
||||
if (num !== focused) num.value = interval;
|
||||
for (const el of [box, num]) {
|
||||
el.closest('.set-row')?.classList.toggle('set-row-disabled', follows);
|
||||
el.disabled = follows;
|
||||
@@ -3085,16 +3091,17 @@ Object.assign(CodemanApp.prototype, {
|
||||
async autoSaveGitFetch() {
|
||||
if (!this.editingSessionId) return;
|
||||
const follow = document.getElementById('modalGitFetchFollow').checked;
|
||||
const num = document.getElementById('modalGitFetchInterval');
|
||||
// `null` on the route means "no choice of your own", which is what following the app setting is.
|
||||
const body = follow
|
||||
? { enabled: null, intervalSeconds: null }
|
||||
: {
|
||||
enabled: document.getElementById('modalGitFetchEnabled').checked,
|
||||
intervalSeconds: Math.min(
|
||||
3600,
|
||||
Math.max(30, parseInt(document.getElementById('modalGitFetchInterval').value, 10) || 300)
|
||||
),
|
||||
intervalSeconds: Math.min(3600, Math.max(30, parseInt(num.value, 10) || 300)),
|
||||
};
|
||||
// Enter fires `onchange` with the field still focused, and updateGitFetchStatus leaves a focused field
|
||||
// alone, so the clamped number is written back here or the field would keep showing what was typed.
|
||||
if (!follow) num.value = body.intervalSeconds;
|
||||
try {
|
||||
// `_apiPost` hands back the Response without checking it, so a refused body (a 400) still read as a
|
||||
// save. `_apiJson` is the one that says whether the call went through, and null here means it did not.
|
||||
|
||||
+36
-27
@@ -7151,33 +7151,6 @@ html[data-skin="og"] .welcome-primary:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
/* A Fetch button cannot be clicked twice while its fetch is running, and is not offered for a session
|
||||
there is nothing to fetch for (no repository, remote or Docker). */
|
||||
.btn-icon-sm[disabled] {
|
||||
color: var(--text-dim);
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.btn-icon-sm[disabled]:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.btn-icon-sm[aria-busy='true'] {
|
||||
color: var(--text);
|
||||
animation: btn-icon-busy 1.1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes btn-icon-busy {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.55;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 1rem;
|
||||
@@ -22175,6 +22148,42 @@ html .toolbar .btn-git-status[aria-expanded='true'] {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* A Fetch button cannot be clicked twice while its fetch is running, and is not offered for a session
|
||||
there is nothing to fetch for (no repository, remote or Docker). Scoped to the Git panel's header: the
|
||||
File Viewer's Back/Forward and the folder grid's Up button are .btn-icon-sm too, and keep their own look. */
|
||||
.git-status-actions .btn-icon-sm[disabled] {
|
||||
color: var(--text-dim);
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.git-status-actions .btn-icon-sm[disabled]:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.git-status-actions .btn-icon-sm[aria-busy='true'] {
|
||||
color: var(--text);
|
||||
animation: btn-icon-busy 1.1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes btn-icon-busy {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.55;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
/* No pulse under reduced motion: the button holds the pulse's brighter step, still dimmer than at rest. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.git-status-actions .btn-icon-sm[aria-busy='true'] {
|
||||
animation: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.git-status-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
+3
-3
@@ -1550,7 +1550,7 @@ export class WebServer extends EventEmitter {
|
||||
): Promise<void> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
this.keepWarmService.remove(sessionId);
|
||||
// A closed case takes its place in the fetch schedule with it: otherwise a detached-and-forgotten id
|
||||
// A closed session takes its place in the fetch schedule with it: otherwise a detached-and-forgotten id
|
||||
// would still count as tracked, and its next run would land on a session that no longer exists.
|
||||
this.gitFetchScheduler.remove(sessionId);
|
||||
const pinned = session?.pinned === true || this.store.getSession(sessionId)?.pinned === true;
|
||||
@@ -3819,9 +3819,9 @@ export class WebServer extends EventEmitter {
|
||||
// below, a future `expiresAt` re-arms the timer once the pane is attached.
|
||||
lastRealPromptAt: savedState?.lastRealPromptAt,
|
||||
keepWarmEnabled: savedState?.keepWarmEnabled,
|
||||
// The case's own fetch switch and interval, carried over rather than reset to the app default:
|
||||
// The session's own fetch switch and interval, carried over rather than reset to the app default:
|
||||
// the choice is the user's and outlives the run that made it. The scheduler reads them fresh
|
||||
// on its next tick, so a restored case is back on its own schedule without a restart.
|
||||
// on its next tick, so a restored session is back on its own schedule without a restart.
|
||||
gitFetchEnabled: savedState?.gitFetchEnabled,
|
||||
gitFetchIntervalSeconds: savedState?.gitFetchIntervalSeconds,
|
||||
// Conversations this pane provably owned, oldest first. Its tail is
|
||||
|
||||
Reference in new issue
Block a user