Terminal newlines, OAuth callback, Claude Code settings, and the Files tab #30
+7
-3
@@ -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
|
**Bypass** a task may stop early when Claude Code asks for approval; the run log records the mode
|
||||||
that was used.
|
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 quickest route is the **New task** button on a project's **Automation** tab, which gives you a
|
||||||
the commands yourself in a **Shell** session, or just ask Claude to do it.
|
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
|
### Create a Recurring Task
|
||||||
|
|
||||||
|
|||||||
@@ -431,6 +431,18 @@
|
|||||||
const mobileInput = document.getElementById('mobileInput');
|
const mobileInput = document.getElementById('mobileInput');
|
||||||
const btnEnter = document.getElementById('btnEnter');
|
const btnEnter = document.getElementById('btnEnter');
|
||||||
const btnNewline = document.getElementById('btnNewline');
|
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 btnTab = document.getElementById('btnTab');
|
||||||
const btnCtrlC = document.getElementById('btnCtrlC');
|
const btnCtrlC = document.getElementById('btnCtrlC');
|
||||||
const scrollBottomBtn = document.getElementById('scrollBottomBtn');
|
const scrollBottomBtn = document.getElementById('scrollBottomBtn');
|
||||||
@@ -791,6 +803,7 @@
|
|||||||
switchToSession(remaining[remaining.length - 1]);
|
switchToSession(remaining[remaining.length - 1]);
|
||||||
} else {
|
} else {
|
||||||
activeSessionId = null;
|
activeSessionId = null;
|
||||||
|
syncNewlineButton();
|
||||||
emptyState.style.display = '';
|
emptyState.style.display = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -817,6 +830,7 @@
|
|||||||
|
|
||||||
function switchToSession(sessionId) {
|
function switchToSession(sessionId) {
|
||||||
activeSessionId = sessionId;
|
activeSessionId = sessionId;
|
||||||
|
syncNewlineButton();
|
||||||
|
|
||||||
// Update tab styles
|
// Update tab styles
|
||||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
@@ -896,7 +910,7 @@
|
|||||||
// reasoning, as the terminal's own key handler above. A hardware
|
// 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
|
// keyboard on a tablet is the only way to reach this; the phone case is
|
||||||
// the dedicated newline button beside Enter.
|
// the dedicated newline button beside Enter.
|
||||||
sendTerminalInput(e.shiftKey ? '\x1b\r' : '\r');
|
sendTerminalInput(e.shiftKey && activeSessionTakesEscCr() ? '\x1b\r' : '\r');
|
||||||
} else if (e.key === 'Tab') {
|
} else if (e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendTerminalInput('\t');
|
sendTerminalInput('\t');
|
||||||
@@ -904,7 +918,27 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
btnEnter.onclick = () => { sendTerminalInput('\r'); mobileInput.focus(); };
|
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(); };
|
btnTab.onclick = () => { sendTerminalInput('\t'); mobileInput.focus(); };
|
||||||
btnCtrlC.onclick = () => { sendTerminalInput('\x03'); mobileInput.focus(); };
|
btnCtrlC.onclick = () => { sendTerminalInput('\x03'); mobileInput.focus(); };
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,14 @@ export default function RuntimeSection({
|
|||||||
|
|
||||||
<ConfigGroup
|
<ConfigGroup
|
||||||
title="Claude Code settings"
|
title="Claude Code settings"
|
||||||
description="Per-project CLI behaviour. Anything left on Global follows Settings; Off overrides a global On."
|
description={
|
||||||
|
"Per-project CLI behaviour. Anything left on Global follows Settings; " +
|
||||||
|
"Off overrides a global On. Turning TUI mode, Effort level or Focus mode " +
|
||||||
|
"back to Global needs the container's base image updated first — those " +
|
||||||
|
"three are cleared by removing a key, and an older image's startup script " +
|
||||||
|
"ignores the instruction to remove it. Update the base image from Overview " +
|
||||||
|
"if one of them will not switch off."
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ClaudeCodeSettingsEditor
|
<ClaudeCodeSettingsEditor
|
||||||
scope="project"
|
scope="project"
|
||||||
|
|||||||
+23
-1
@@ -491,9 +491,31 @@ if [ -f "$CLAUDE_JSON" ]; then
|
|||||||
# Only rewrite when the value isn't already true, to avoid a needless jq
|
# Only rewrite when the value isn't already true, to avoid a needless jq
|
||||||
# reformat of ~/.claude.json on every single start.
|
# reformat of ~/.claude.json on every single start.
|
||||||
if ! grep -q '"shiftEnterKeyBindingInstalled"[[:space:]]*:[[:space:]]*true' "$CLAUDE_JSON" 2>/dev/null; then
|
if ! grep -q '"shiftEnterKeyBindingInstalled"[[:space:]]*:[[:space:]]*true' "$CLAUDE_JSON" 2>/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)
|
MERGED=$(jq '.shiftEnterKeyBindingInstalled = true' "$CLAUDE_JSON" 2>/dev/null)
|
||||||
if [ -n "$MERGED" ]; then
|
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
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user