diff --git a/HOW-TO-USE.md b/HOW-TO-USE.md index d744049..1acf536 100644 --- a/HOW-TO-USE.md +++ b/HOW-TO-USE.md @@ -1216,10 +1216,14 @@ change. Remember that a headless run cannot answer a permission prompt, so in an **Bypass** a task may stop early when Claude Code asks for approval; the run log records the mode that was used. -### Creating Tasks (In the Container) +### Creating Tasks -There is no "add task" form in the app. Create tasks from a terminal in the container — either type -the commands yourself in a **Shell** session, or just ask Claude to do it. +The quickest route is the **New task** button on a project's **Automation** tab, which gives you a +form for the name, the schedule and the prompt. + +You can also create tasks from a terminal in the container — type the commands yourself in a +**Shell** session, or just ask Claude to do it. That is the better route when you want Claude to +work out the schedule or the prompt for you, and it is what the rest of this section covers. ### Create a Recurring Task diff --git a/app/src-tauri/src/web_terminal/terminal.html b/app/src-tauri/src/web_terminal/terminal.html index 98248f9..6527294 100644 --- a/app/src-tauri/src/web_terminal/terminal.html +++ b/app/src-tauri/src/web_terminal/terminal.html @@ -431,6 +431,18 @@ const mobileInput = document.getElementById('mobileInput'); const btnEnter = document.getElementById('btnEnter'); const btnNewline = document.getElementById('btnNewline'); + + // Whether the *active* session understands ESC+CR as "insert a newline". + // + // Only Claude Code does. `bash -l` has no readline binding for `\e\r`, so + // sending it there is a silent no-op — which is worse from the mobile bar + // than from a hardware key, because the bar puts a dedicated button on + // screen that appears to do nothing. The xterm key handler is already scoped + // this way; these two paths were not. + function activeSessionTakesEscCr() { + const s = activeSessionId && sessions[activeSessionId]; + return !!s && s.type === 'claude'; + } const btnTab = document.getElementById('btnTab'); const btnCtrlC = document.getElementById('btnCtrlC'); const scrollBottomBtn = document.getElementById('scrollBottomBtn'); @@ -791,6 +803,7 @@ switchToSession(remaining[remaining.length - 1]); } else { activeSessionId = null; + syncNewlineButton(); emptyState.style.display = ''; } } @@ -817,6 +830,7 @@ function switchToSession(sessionId) { activeSessionId = sessionId; + syncNewlineButton(); // Update tab styles document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); @@ -896,7 +910,7 @@ // reasoning, as the terminal's own key handler above. A hardware // keyboard on a tablet is the only way to reach this; the phone case is // the dedicated newline button beside Enter. - sendTerminalInput(e.shiftKey ? '\x1b\r' : '\r'); + sendTerminalInput(e.shiftKey && activeSessionTakesEscCr() ? '\x1b\r' : '\r'); } else if (e.key === 'Tab') { e.preventDefault(); sendTerminalInput('\t'); @@ -904,7 +918,27 @@ }); btnEnter.onclick = () => { sendTerminalInput('\r'); mobileInput.focus(); }; - btnNewline.onclick = () => { sendTerminalInput('\x1b\r'); mobileInput.focus(); }; + btnNewline.onclick = () => { + if (!activeSessionTakesEscCr()) { mobileInput.focus(); return; } + sendTerminalInput('\x1b\r'); + mobileInput.focus(); + }; + + // Keep the button's affordance honest: on a shell tab there is no byte that + // means "newline without running the line", so the control is disabled + // rather than left looking live. + function syncNewlineButton() { + const usable = activeSessionTakesEscCr(); + btnNewline.disabled = !usable; + btnNewline.title = usable + ? 'Insert a newline without submitting (Shift+Enter)' + : 'Only Claude sessions support this — a shell runs the line instead'; + } + + // With no session open yet, `activeSessionTakesEscCr()` is already false — + // but nothing had called this, so the button rendered live before the first + // tab existed. + syncNewlineButton(); btnTab.onclick = () => { sendTerminalInput('\t'); mobileInput.focus(); }; btnCtrlC.onclick = () => { sendTerminalInput('\x03'); mobileInput.focus(); }; diff --git a/app/src/components/projects/home/config/RuntimeSection.tsx b/app/src/components/projects/home/config/RuntimeSection.tsx index c60868e..9bbb19e 100644 --- a/app/src/components/projects/home/config/RuntimeSection.tsx +++ b/app/src/components/projects/home/config/RuntimeSection.tsx @@ -109,7 +109,14 @@ export default function RuntimeSection({ /dev/null; then + # Write to a temp file and rename, never `> "$CLAUDE_JSON"`. + # + # `>` truncates before it writes, so a write that fails part-way — a + # full home volume is the obvious way, and bounding that volume is + # what half this release is about — leaves the file unparseable. This + # file holds the OAuth account, and the damage does not self-heal: the + # next start's `jq` fails on the corrupt file, `MERGED` is empty, and + # the guard below skips the write that would have repaired it. So the + # failure mode is a permanently lost login, for a purely cosmetic flag + # that suppresses a "run /terminal-setup" tip. + # + # `triple-c-task-runner` already does it this way; this block was + # modelled on the awsAuthRefresh one above, which has the same flaw but + # only fires when a Bedrock SSO command is configured. MERGED=$(jq '.shiftEnterKeyBindingInstalled = true' "$CLAUDE_JSON" 2>/dev/null) if [ -n "$MERGED" ]; then - printf '%s\n' "$MERGED" > "$CLAUDE_JSON" + CLAUDE_JSON_TMP="${CLAUDE_JSON}.triple-c-tmp" + if printf '%s\n' "$MERGED" > "$CLAUDE_JSON_TMP" 2>/dev/null; then + mv -f "$CLAUDE_JSON_TMP" "$CLAUDE_JSON" 2>/dev/null || rm -f "$CLAUDE_JSON_TMP" + else + # Out of space, or the volume went read-only. The original is + # untouched, which is the whole point. + rm -f "$CLAUDE_JSON_TMP" + echo "entrypoint: warning — could not set shiftEnterKeyBindingInstalled (leaving ~/.claude.json as it was)" + fi fi fi else