Skip to content

Commit d89ebb6

Browse files
IMNMVclaude
andcommitted
Add unified console logging (issue #27)
ClaudeR 0.13.0. The session log captured agent activity only, so a user who hit an error in the console had no direct way to hand that context to an agent short of re-running the work through it. New start_console_logging() / stop_console_logging(), plus an "Also log my own console commands" toggle under Logging. Console entries land in the existing session log tagged "Run by user (console)" beside the agent entries: the expression, its visible value, and any warnings, messages, or errors. Captured with addTaskCallback for the expression and value, and globalCallingHandlers for conditions. The handlers observe only and never invoke the muffle restarts, so the user still sees their own warnings and messages. globalCallingHandlers refuses to run with handlers on the stack, so those calls are deliberately not wrapped in tryCatch, and the previous handler list is restored on stop so handlers owned by other packages survive. sink(type = "message") is not used: it cannot split, so it would swallow the console output it captures. Off by default. R CMD check: Status OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent eabdee7 commit d89ebb6

7 files changed

Lines changed: 173 additions & 1 deletion

File tree

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: ClaudeR
22
Title: R Integration for Claude AI
3-
Version: 0.12.6
3+
Version: 0.13.0
44
Authors@R: person("Nykko", "Vitali", email = "nykvt@icloud.com", role = c("aut", "cre"))
55
Description: Connects RStudio with Claude AI to enable interactive coding sessions.
66
License: MIT + file LICENSE

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export(reviewer_zero_prompt)
4242
export(revoke_plan)
4343
export(screening_prompt)
4444
export(screening_report)
45+
export(start_console_logging)
46+
export(stop_console_logging)
4547
export(validate_assembly_round)
4648
importFrom(base64enc,base64encode)
4749
importFrom(ggplot2,aes)

R/console_log.R

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Console capture: record what the USER runs in the console into the same
2+
# session log the agent writes to, so one file holds both sides of the work.
3+
#
4+
# The pieces, and why each is needed:
5+
# addTaskCallback the expression the user typed, and its visible value
6+
# globalCallingHandlers warnings and messages (they go to stderr, so a sink
7+
# cannot see them; R >= 4.0 lets us observe without
8+
# suppressing)
9+
# options(error=) uncaught errors
10+
# sink(type = "message") is deliberately NOT used: it cannot split, so it would
11+
# swallow the user's own errors in the console.
12+
13+
.console_state <- new.env(parent = emptyenv())
14+
15+
console_log_path <- function() {
16+
s <- tryCatch(.claude_server_env$settings, error = function(e) NULL)
17+
if (is.null(s) || !isTRUE(s$log_to_file)) return(NULL)
18+
p <- s$log_file_path
19+
if (is.null(p) || !nzchar(p)) return(NULL)
20+
p
21+
}
22+
23+
# One writer for every console entry, so the format stays consistent.
24+
console_write <- function(text, tag = "user") {
25+
p <- console_log_path()
26+
if (is.null(p)) return(invisible(FALSE))
27+
ts <- format(Sys.time(), "%Y-%m-%d %H:%M:%S")
28+
entry <- sprintf("# --- [%s] ---\n# Run by %s (console):\n%s\n\n", ts, tag, text)
29+
tryCatch(cat(entry, file = p, append = TRUE), error = function(e) NULL)
30+
invisible(TRUE)
31+
}
32+
33+
# Trim runaway output so one huge print cannot bloat the log.
34+
console_trim <- function(x, max_lines = 40L) {
35+
if (length(x) <= max_lines) return(x)
36+
c(x[seq_len(max_lines)],
37+
sprintf("# ... %d more lines not logged", length(x) - max_lines))
38+
}
39+
40+
console_note_condition <- function(kind, msg) {
41+
msg <- trimws(paste(msg, collapse = " "))
42+
if (!nzchar(msg)) return(invisible(NULL))
43+
.console_state$pending <- c(.console_state$pending,
44+
sprintf("# %s: %s", kind, msg))
45+
invisible(NULL)
46+
}
47+
48+
console_task_callback <- function(expr, value, ok, visible) {
49+
# Never let logging break the user's session.
50+
tryCatch({
51+
if (is.null(console_log_path())) return(TRUE)
52+
code <- paste(deparse(expr), collapse = "\n")
53+
54+
# Skip our own bookkeeping so the log does not describe itself.
55+
if (grepl("^(start_console_logging|stop_console_logging|ClaudeR:::)", code)) return(TRUE)
56+
57+
lines <- character(0)
58+
if (isTRUE(visible) && isTRUE(ok)) {
59+
out <- tryCatch(utils::capture.output(print(value)),
60+
error = function(e) character(0))
61+
if (length(out)) lines <- paste("#", console_trim(out))
62+
}
63+
if (!isTRUE(ok)) lines <- c(lines, "# error: command did not complete")
64+
if (length(.console_state$pending)) {
65+
lines <- c(.console_state$pending, lines)
66+
.console_state$pending <- NULL
67+
}
68+
body <- if (length(lines)) paste0(code, "\n", paste(lines, collapse = "\n")) else code
69+
console_write(body, tag = "user")
70+
}, error = function(e) NULL)
71+
TRUE
72+
}
73+
74+
#' Start logging the user's console activity
75+
#'
76+
#' Adds the user's own console commands, and their results, to the session log
77+
#' the agent already writes to. An agent can then read one file and see both
78+
#' sides of the session.
79+
#'
80+
#' @return Invisibly TRUE if capture started.
81+
#' @export
82+
start_console_logging <- function() {
83+
if (isTRUE(.console_state$active)) return(invisible(TRUE))
84+
.console_state$pending <- NULL
85+
.console_state$handle <- addTaskCallback(console_task_callback,
86+
name = "clauder_console_log")
87+
if (getRversion() >= "4.0.0") {
88+
# globalCallingHandlers() refuses to run with handlers on the stack, so it
89+
# must not be wrapped in tryCatch here.
90+
.console_state$old_handlers <- globalCallingHandlers()
91+
# Observe only. Do NOT invoke the muffle restarts: the user must still see
92+
# their own warnings and messages in the console.
93+
globalCallingHandlers(
94+
warning = function(w) console_note_condition("warning", conditionMessage(w)),
95+
message = function(m) console_note_condition("message", conditionMessage(m))
96+
)
97+
}
98+
.console_state$old_error <- getOption("error")
99+
options(error = function() {
100+
tryCatch({
101+
msg <- geterrmessage()
102+
console_write(sprintf("# error: %s", trimws(msg)), tag = "user")
103+
}, error = function(e) NULL)
104+
})
105+
.console_state$active <- TRUE
106+
message("ClaudeR: console logging on. Your console commands now appear in the session log.")
107+
# Our own startup message must not show up as the user's first log entry.
108+
.console_state$pending <- NULL
109+
invisible(TRUE)
110+
}
111+
112+
#' Stop logging the user's console activity
113+
#'
114+
#' @return Invisibly TRUE.
115+
#' @export
116+
stop_console_logging <- function() {
117+
if (!isTRUE(.console_state$active)) return(invisible(TRUE))
118+
tryCatch(removeTaskCallback("clauder_console_log"), error = function(e) NULL)
119+
if (getRversion() >= "4.0.0") {
120+
# Drop ours, then put back whatever was registered before, so handlers
121+
# belonging to other packages survive.
122+
globalCallingHandlers(NULL)
123+
old <- .console_state$old_handlers
124+
if (length(old)) do.call(globalCallingHandlers, old)
125+
}
126+
options(error = .console_state$old_error)
127+
.console_state$active <- FALSE
128+
.console_state$pending <- NULL
129+
message("ClaudeR: console logging off.")
130+
invisible(TRUE)
131+
}

R/ui.R

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,8 @@ claudeAddin <- function() {
734734
condition = "input.log_to_file == true",
735735
textInput("log_file_path", "Log file path",
736736
value = settings$log_file_path),
737+
checkboxInput("log_console", "Also log my own console commands",
738+
value = isTRUE(settings$log_console)),
737739
actionButton("open_log", "Open Log File", class = "btn-sm"),
738740
actionButton("export_script", "Export Clean Script", class = "btn-sm")
739741
)
@@ -790,6 +792,11 @@ claudeAddin <- function() {
790792
observeEvent(input$print_to_console, {
791793
update_setting("print_to_console", input$print_to_console)
792794
}, ignoreInit = TRUE)
795+
observeEvent(input$log_console, {
796+
update_setting("log_console", input$log_console)
797+
if (isTRUE(input$log_console)) start_console_logging() else stop_console_logging()
798+
}, ignoreInit = TRUE)
799+
793800
observeEvent(input$log_to_file, {
794801
update_setting("log_to_file", input$log_to_file)
795802
}, ignoreInit = TRUE)

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ claudeAddin()
5050
<details>
5151
<summary><b>Recent Updates</b> (click to expand)</summary>
5252

53+
- **Unified console logging (R 0.13.0).** The session log recorded what the agent ran, but not what you ran, so asking an agent to explain an error you hit in the console meant re-running the work through it. Tick "Also log my own console commands" under Logging and your console activity joins the same file, tagged by who ran what: the command, its printed result, and any warnings, messages, or errors. "Read the last 100 lines of the log" is now enough for an agent to see both sides of the session. Off by default; toggle it in the addin or call `start_console_logging()` / `stop_console_logging()`.
54+
5355
- **Editor tools fixed, plus approve-before-apply edits (R 0.12.5 / clauder-mcp 0.14.4).** From two user reports. `modify_code_section` and `insert_text` now save to disk by default and report `saved_to_disk`, so an agent no longer believes it wrote a file when the change sat unsaved in the buffer. Bounded replacements may change the line count (the old equality constraint is gone). Both tools accept a `path` to target a specific file, open and focus it, and refuse to edit a different document instead of failing silently. `get_active_document` now reports the path, the document id, and whether the buffer differs from disk, so buffer state and file state stop being confused for each other. New `suggest_edit` tool: the agent proposes a change and waits for the user to approve it, using `rstudioapi::showEditSuggestion()` when the installed rstudioapi provides it, and otherwise staging the edit unsaved so the user accepts by saving or rejects with undo.
5456

5557
- **Reviewer Zero now reasons, not just reconciles (R 0.12.4).** Added a mandatory Pass 5 (content reasoning) to the base auditing protocol. The deterministic tools (reconcile_values, verify_references, check_cross_references, probe_scripts) find numeric, reference, cross-reference, and code defects, but they do not reason about meaning, and a manuscript can clear every one of them and still be wrong. Pass 5 is a gated, equal-weight pass with eight checks the tools cannot do: instrument and source attribution, whether each reported test is computable from the data that exists, whether each cited figure or table actually contains the claimed evidence, magnitude wording, convergence across studies, causal and generality framing, data existence for descriptive claims, and supplement and appendix integration (every supplement cited, and no body claim resting on an uncited one). In a controlled benchmark on a synthetic manuscript with a known defect set, this raised detection from below a native-tools baseline (16.8 of 24) to clearly above it (21.3 of 24), recovering exactly the reasoning defects the old tool-led protocol was missing, with no rise in false positives.

man/start_console_logging.Rd

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/stop_console_logging.Rd

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)