mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 00:19:42 +02:00
feat(adopt): adopt tmux sessions a human started outside Codeman
The home screen now lists tmux sessions Codeman did not start (a claude or codex running inside `tmux new -s work`, or just a shell); one click turns one into a tab you can keep working in. Adoption is a fourth LOCATION OVERLAY, structurally identical to remote/docker, and NOT a new SessionMode: the outer layer is still an ordinary codeman-<8hex> wrapper session on this instance's own socket, and only the pane inside it runs the attach. Session-name allowlisting, capture, input and the recovery chain are therefore untouched, and "detach, never kill the foreign session" becomes structural rather than a rule to remember — killSession can only ever reach our own wrapper. All three locations share one probe script, one parser and one classifier, and differ only in the shell around them (direct exec / docker exec / ssh). Pane mode is decided from the bounded process-tree argv of pane_pid, because claude and codex both report `node` as pane_current_command; anything unrecognised is treated as a shell. Things measured rather than assumed: - A grouped session buys only `status off` and an independent current window, not an independent size. Measured on tmux 3.3a: both a bare attach and a grouped one shrink the other client's 200x49 to 80x23. Only `window-size largest` preserves it, but that is a shared window option that survives our departure, so it is not set. - View reclamation: local relies on client death, ssh on SIGHUP, but a `docker exec` does not die with its client — the container-side view must be reclaimed explicitly on kill or every adoption leaks one. - The session name is chosen by someone else, while the local launch chain ends in `bash -c` plus JSON.stringify, which does not escape `$` or backticks, so the outer shell performs substitution before the inner single quotes close. Session names and socket paths therefore pass a character allowlist and are discarded during DISCOVERY, so a non-conforming candidate never gets an id. Capability degrades by "who started this process": an adopted session has no hooks, no envOverrides and no effort, and its working directory is merely the foreign pane's cwd at that moment (possibly not even on this host). Respawn, Ralph, the orchestrator, hook waits, and every watcher that tails the local filesystem by workingDir are refused or skipped, and the close dialog no longer offers a "kill the session" option it cannot honour.
This commit is contained in:
@@ -17618,3 +17618,173 @@ html[data-session-list="sidebar"][data-sidebar="collapsed"] .btn-sidebar-toggle
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
Foreign sessions — tmux sessions a human started outside Codeman
|
||||
(foreign-sessions.js). Rendered on the welcome screen and, with the
|
||||
same row builder, inside the phone overview.
|
||||
|
||||
Colour vocabulary is deliberately the session-tab one: a mode dot on
|
||||
the left, name over a dim meta line, action pinned right. A block that
|
||||
invented its own language here would read as a different product.
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
.welcome-foreign {
|
||||
width: 100%;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.foreign-sessions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* `.foreign-sessions` is a flex container, so `[hidden]` needs re-asserting or
|
||||
the module's only visibility lever does nothing (same trap as .home-sessions). */
|
||||
.foreign-sessions[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.foreign-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.foreign-title {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.foreign-count {
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-dim);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 999px;
|
||||
padding: 0.1rem 0.45rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.foreign-scan-toggle {
|
||||
margin-left: auto;
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-dim);
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 0.12rem 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.foreign-scan-toggle[aria-pressed='true'] {
|
||||
color: var(--session-blue, #4a9eff);
|
||||
border-color: var(--session-blue, #4a9eff);
|
||||
}
|
||||
|
||||
.foreign-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
max-height: min(40vh, 320px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.foreign-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
padding: 0.4rem 0.55rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.foreign-row--open {
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.foreign-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex: 0 0 auto;
|
||||
background: var(--text-muted, #888);
|
||||
}
|
||||
|
||||
.foreign-dot--claude {
|
||||
background: #d97757;
|
||||
}
|
||||
.foreign-dot--codex {
|
||||
background: #9b8cff;
|
||||
}
|
||||
.foreign-dot--shell {
|
||||
background: #4caf7d;
|
||||
}
|
||||
|
||||
.foreign-row-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.foreign-row-name {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.foreign-row-sub {
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-dim);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.foreign-open-btn {
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.22rem 0.6rem;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.foreign-open-btn:hover:not(:disabled) {
|
||||
border-color: var(--session-blue, #4a9eff);
|
||||
color: var(--session-blue, #4a9eff);
|
||||
}
|
||||
|
||||
.foreign-open-btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.foreign-empty {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-dim);
|
||||
padding: 0.3rem 0.1rem;
|
||||
}
|
||||
|
||||
.foreign-notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.foreign-note {
|
||||
font-size: 0.66rem;
|
||||
color: var(--text-dim);
|
||||
opacity: 0.85;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user