mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 01:48:20 -10:00
Merge origin/main into fix/live-model-context-limits-upstream
Picks up #7108, which restores main's clippy and rustfmt gate (CW-7525). Signed-off-by: CodeWhale Bot <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
233f590fe5
89 files changed
+1906
-1119
No files matched your search
@@ -63,6 +63,7 @@ crates/tui/extension-host/dist/** text eol=lf
|
||||
# Reviewed extension-host fixtures carry exact source SHA-256 receipts.
|
||||
# A Windows checkout must not rewrite their admitted source closure to CRLF.
|
||||
crates/tui/tests/fixtures/extension_host/** text eol=lf
|
||||
crates/tui/extension-host/src/dsh/dsh-message-compat.js text eol=lf
|
||||
|
||||
# Rustfmt writes LF; keep Rust sources stable across Windows/Linux/macOS.
|
||||
*.rs text eol=lf
|
||||
|
||||
@@ -270,3 +270,5 @@ Henrik Bakke Dukefoss = Henrik Bakke Dukefoss <[email protected]
|
||||
[email protected] = Henrik Bakke Dukefoss <[email protected]>
|
||||
|
||||
[email protected] = Dhruv Singal <[email protected]>
|
||||
[email protected] = Boyce <[email protected]>
|
||||
Boyce = Boyce <[email protected]>
|
||||
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **MCP catalogs stay fresh:** a server's tools, resources, prompts and skills are rediscovered on their own connection when an advertised `ttlMs` expires or the server sends a list-changed notice. A tool resolved against the old catalog is refused with "retry the call", a removed tool cannot run, and a failed refresh reconnects instead of serving stale entries. Catalogs are never shared across servers, credentials or reconnects. (CW-7547)
|
||||
- **MCP Skills extension:** servers that advertise the published Skills extension have their skills listed in `list_mcp_resources`. Listing never activates a skill, and reading a skill file is refused unless its size and sha256 digest match the listing. MCP skills do not yet join `/skills`. (CW-7545)
|
||||
- **Computer Use is installed, not bundled:** the Computer Use plugin is no longer vendored into the binary. Install it from the `codewhale` marketplace, which points at its independent release; its consent tools load on first use, and decisions still require a human approval card. (CW-6732)
|
||||
- **First-party marketplace receipt:** the shipped catalog is pinned to marketplace revision `a5f1d210372ff48c9a278a0a39737956ac0f2d93`.
|
||||
- **Tool image validation:** an image returned by a tool must decode before it is sent, so truncated or corrupt files are omitted with a receipt even when they are already within the delivery budget.
|
||||
|
||||
- **Image-heavy session recovery:** ordinary image reads use a 256 KiB delivery budget. Requests measure serialized bytes, omit older images from an outbound copy with receipts, and retry an initial HTTP 413 once only with a smaller body. Saved images and executed tool actions remain intact. (CW-7550)
|
||||
@@ -210,6 +211,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
is now interrupted instead of running on with nothing able to stop it
|
||||
(#6558).
|
||||
|
||||
- Multiline mode now has a send key on every terminal: with
|
||||
`composer_multiline_mode = true`, `Enter` inserts a newline and `Alt-Enter`
|
||||
sends. `Shift-Enter` also sends where the terminal reports it distinctly; on
|
||||
Linux terminals that report it as plain `Enter`, multiline mode previously
|
||||
had no way to send. `Ctrl-J` always inserts a newline. Thanks
|
||||
[@luestr](https://github.com/luestr) for the report. (#7093)
|
||||
- Session saves no longer fail with "Fleet artifact path must stay within the
|
||||
workspace" when the Codewhale state root is a symlink or Windows junction to
|
||||
another volume: the state root is resolved before session-side state is
|
||||
opened. Thanks [@SparkofSpike](https://github.com/SparkofSpike). (#7097)
|
||||
- README translations carry refreshed source stamps after the 0.10.3 batch.
|
||||
Thanks [@Aboysky](https://github.com/Aboysky). (#7104)
|
||||
|
||||
### Contributors
|
||||
|
||||
- **[@7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — per-scope instruction budgets with visible truncation ([#7086](https://github.com/codewhale-hq/Codewhale/pull/7086), [#6526](https://github.com/codewhale-hq/Codewhale/issues/6526)).
|
||||
- **[@Lstarsky0](https://github.com/Lstarsky0)** — kept the `/diff` pager's place across a resize ([#7087](https://github.com/codewhale-hq/Codewhale/pull/7087)), said "Goal cleared." only when there was a goal ([#7089](https://github.com/codewhale-hq/Codewhale/pull/7089)), and stopped printing an unpriced session cost as `$0.0000` in `/status` ([#7090](https://github.com/codewhale-hq/Codewhale/pull/7090)).
|
||||
- **[@gaord](https://github.com/gaord)** — a running turn's cost and context in the Runtime ([#7074](https://github.com/codewhale-hq/Codewhale/pull/7074)).
|
||||
- **[Sh1Zuku / @SparkofSpike](https://github.com/SparkofSpike)** — resolved a relocated state root before session-side state opens ([#7097](https://github.com/codewhale-hq/Codewhale/pull/7097)).
|
||||
- **[Boyce / @Aboysky](https://github.com/Aboysky)** — refreshed the README translation source stamps ([#7104](https://github.com/codewhale-hq/Codewhale/pull/7104)).
|
||||
- **[@M-Maciej](https://github.com/M-Maciej)** — reported answerless output-limit stops in long Fleet proofs ([#6990](https://github.com/codewhale-hq/Codewhale/issues/6990)).
|
||||
- **[@asto18089](https://github.com/asto18089)** — diagnosed the NoNewPrivs child-matrix failure on hardened containers ([#7021](https://github.com/codewhale-hq/Codewhale/issues/7021)).
|
||||
- **[@luestr](https://github.com/luestr)** — reported the Linux multiline send key ([#7093](https://github.com/codewhale-hq/Codewhale/issues/7093)) and session export guidance ([#6996](https://github.com/codewhale-hq/Codewhale/issues/6996)).
|
||||
|
||||
## [0.10.2] - 2026-10-22
|
||||
|
||||
Codewhale v0.10.2 adds an animated pet view (`/pet on`), a live Terminal work
|
||||
|
||||
@@ -208,7 +208,7 @@ pub struct ReviewOperation {
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum Op {
|
||||
Review {
|
||||
turn: TurnSpec,
|
||||
turn: Box<TurnSpec>,
|
||||
request: ReviewOperation,
|
||||
},
|
||||
/// Drive one model turn: `role=user` content plus the resolved route
|
||||
@@ -629,7 +629,7 @@ mod tests {
|
||||
};
|
||||
vec![
|
||||
Op::Review {
|
||||
turn,
|
||||
turn: Box::new(turn),
|
||||
request: ReviewOperation {
|
||||
version: 1,
|
||||
repository: "owner/repo".into(),
|
||||
|
||||
@@ -49,6 +49,12 @@ impl ModelEnvGuard {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ModelEnvGuard {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ModelEnvGuard {
|
||||
fn drop(&mut self) {
|
||||
// Safety: test-only environment restoration under the same lock.
|
||||
|
||||
+160
-160
@@ -393,6 +393,166 @@ fn error_word_is_allowlisted(word: &str) -> bool {
|
||||
!looks_opaque(word)
|
||||
}
|
||||
|
||||
const MAX_RECEIPT_FIELD_CHARS: usize = 96;
|
||||
|
||||
pub fn safe_receipt_field(raw: &str) -> String {
|
||||
let sanitized = sanitize_persisted_route_label(raw);
|
||||
let mut out = String::with_capacity(raw.len().min(MAX_RECEIPT_FIELD_CHARS));
|
||||
let mut previous_separator = false;
|
||||
for ch in sanitized.chars() {
|
||||
if out.chars().count() >= MAX_RECEIPT_FIELD_CHARS {
|
||||
break;
|
||||
}
|
||||
let safe = if ch.is_alphanumeric() || matches!(ch, '.' | '_' | '-' | '/' | ':' | '+') {
|
||||
ch
|
||||
} else {
|
||||
'_'
|
||||
};
|
||||
let separator = safe == '_';
|
||||
if separator && previous_separator {
|
||||
continue;
|
||||
}
|
||||
out.push(safe);
|
||||
previous_separator = separator;
|
||||
}
|
||||
if out.is_empty() { "-".to_string() } else { out }
|
||||
}
|
||||
|
||||
pub fn sanitize_persisted_route_label(raw: &str) -> String {
|
||||
const MAX_PERSISTED_ROUTE_LABEL_CHARS: usize = 256;
|
||||
let value = raw.trim();
|
||||
let lower = value.to_ascii_lowercase();
|
||||
|
||||
if value.is_empty() {
|
||||
return "-".to_string();
|
||||
}
|
||||
|
||||
// URLs are not route labels. Endpoints have a dedicated, validated hash
|
||||
// field; persisting a URL here risks leaking userinfo, query credentials,
|
||||
// or fragments through a custom provider/model name.
|
||||
if value.contains("://") {
|
||||
return "redacted-url".to_string();
|
||||
}
|
||||
|
||||
let authorization_value = ["bearer ", "basic ", "digest ", "token ", "apikey "]
|
||||
.iter()
|
||||
.any(|scheme| lower.starts_with(scheme))
|
||||
|| lower.contains("authorization:")
|
||||
|| lower.contains("proxy-authorization:");
|
||||
if authorization_value {
|
||||
return "redacted-credential".to_string();
|
||||
}
|
||||
|
||||
// Reject credential assignments regardless of common casing or separator:
|
||||
// FOO_API_KEY=..., access-token:..., password = ....
|
||||
for (index, ch) in value.char_indices() {
|
||||
if !matches!(ch, '=' | ':') {
|
||||
continue;
|
||||
}
|
||||
let name = lower[..index]
|
||||
.trim()
|
||||
.trim_matches(|ch: char| matches!(ch, '"' | '\'' | '{' | '[' | ','));
|
||||
let name = name.rsplit([' ', ',', ';']).next().unwrap_or(name);
|
||||
let normalized = name.replace('-', "_");
|
||||
if normalized.ends_with("api_key")
|
||||
|| normalized.ends_with("token")
|
||||
|| normalized.ends_with("secret")
|
||||
|| normalized.ends_with("password")
|
||||
|| normalized.ends_with("passwd")
|
||||
{
|
||||
return "redacted-credential".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Common credential token prefixes. These are intentionally checked at
|
||||
// word boundaries so model ids containing an incidental "sk" survive.
|
||||
let credential_prefix = lower
|
||||
.split(|ch: char| ch.is_whitespace() || matches!(ch, '=' | ':' | ',' | ';' | '"' | '\''))
|
||||
.filter(|part| !part.is_empty())
|
||||
.any(|part| {
|
||||
[
|
||||
"sk-",
|
||||
"sk_",
|
||||
"rk-",
|
||||
"pk-",
|
||||
"ghp_",
|
||||
"gho_",
|
||||
"ghu_",
|
||||
"ghs_",
|
||||
"github_pat_",
|
||||
"hf_",
|
||||
"glpat-",
|
||||
"xoxb-",
|
||||
"xoxp-",
|
||||
"xoxa-",
|
||||
"akia",
|
||||
"aiza",
|
||||
"eyj",
|
||||
]
|
||||
.iter()
|
||||
.any(|prefix| part.starts_with(prefix))
|
||||
});
|
||||
if credential_prefix {
|
||||
return "redacted-credential".to_string();
|
||||
}
|
||||
|
||||
let windows_absolute = value.as_bytes().get(1) == Some(&b':')
|
||||
&& value
|
||||
.as_bytes()
|
||||
.get(2)
|
||||
.is_some_and(|separator| matches!(separator, b'/' | b'\\'));
|
||||
let contains_local_root = [
|
||||
"/users/",
|
||||
"/volumes/",
|
||||
"/home/",
|
||||
"/private/",
|
||||
"\\users\\",
|
||||
"file://",
|
||||
"/.ssh/",
|
||||
"\\.ssh\\",
|
||||
]
|
||||
.iter()
|
||||
.any(|needle| lower.contains(needle));
|
||||
let looks_like_relative_path = value.contains('\\')
|
||||
|| lower.starts_with(".ssh/")
|
||||
|| lower.starts_with(".ssh\\")
|
||||
|| lower.split('/').any(|segment| {
|
||||
matches!(
|
||||
segment,
|
||||
"." | ".."
|
||||
| ".ssh"
|
||||
| ".config"
|
||||
| "secrets"
|
||||
| "secret"
|
||||
| "credentials"
|
||||
| "credential"
|
||||
| "relative"
|
||||
| "workspace"
|
||||
| "tmp"
|
||||
)
|
||||
});
|
||||
if std::path::Path::new(value).is_absolute()
|
||||
|| windows_absolute
|
||||
|| value.starts_with("~/")
|
||||
|| value.starts_with("./")
|
||||
|| value.starts_with("../")
|
||||
|| contains_local_root
|
||||
|| looks_like_relative_path
|
||||
{
|
||||
return "redacted-local-path".to_string();
|
||||
}
|
||||
let bounded: String = value
|
||||
.chars()
|
||||
.filter(|ch| !ch.is_control())
|
||||
.take(MAX_PERSISTED_ROUTE_LABEL_CHARS)
|
||||
.collect();
|
||||
if bounded.is_empty() {
|
||||
"-".to_string()
|
||||
} else {
|
||||
bounded
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -569,163 +729,3 @@ mod tests {
|
||||
assert!(!safe.contains('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_RECEIPT_FIELD_CHARS: usize = 96;
|
||||
|
||||
pub fn safe_receipt_field(raw: &str) -> String {
|
||||
let sanitized = sanitize_persisted_route_label(raw);
|
||||
let mut out = String::with_capacity(raw.len().min(MAX_RECEIPT_FIELD_CHARS));
|
||||
let mut previous_separator = false;
|
||||
for ch in sanitized.chars() {
|
||||
if out.chars().count() >= MAX_RECEIPT_FIELD_CHARS {
|
||||
break;
|
||||
}
|
||||
let safe = if ch.is_alphanumeric() || matches!(ch, '.' | '_' | '-' | '/' | ':' | '+') {
|
||||
ch
|
||||
} else {
|
||||
'_'
|
||||
};
|
||||
let separator = safe == '_';
|
||||
if separator && previous_separator {
|
||||
continue;
|
||||
}
|
||||
out.push(safe);
|
||||
previous_separator = separator;
|
||||
}
|
||||
if out.is_empty() { "-".to_string() } else { out }
|
||||
}
|
||||
|
||||
pub fn sanitize_persisted_route_label(raw: &str) -> String {
|
||||
const MAX_PERSISTED_ROUTE_LABEL_CHARS: usize = 256;
|
||||
let value = raw.trim();
|
||||
let lower = value.to_ascii_lowercase();
|
||||
|
||||
if value.is_empty() {
|
||||
return "-".to_string();
|
||||
}
|
||||
|
||||
// URLs are not route labels. Endpoints have a dedicated, validated hash
|
||||
// field; persisting a URL here risks leaking userinfo, query credentials,
|
||||
// or fragments through a custom provider/model name.
|
||||
if value.contains("://") {
|
||||
return "redacted-url".to_string();
|
||||
}
|
||||
|
||||
let authorization_value = ["bearer ", "basic ", "digest ", "token ", "apikey "]
|
||||
.iter()
|
||||
.any(|scheme| lower.starts_with(scheme))
|
||||
|| lower.contains("authorization:")
|
||||
|| lower.contains("proxy-authorization:");
|
||||
if authorization_value {
|
||||
return "redacted-credential".to_string();
|
||||
}
|
||||
|
||||
// Reject credential assignments regardless of common casing or separator:
|
||||
// FOO_API_KEY=..., access-token:..., password = ....
|
||||
for (index, ch) in value.char_indices() {
|
||||
if !matches!(ch, '=' | ':') {
|
||||
continue;
|
||||
}
|
||||
let name = lower[..index]
|
||||
.trim()
|
||||
.trim_matches(|ch: char| matches!(ch, '"' | '\'' | '{' | '[' | ','));
|
||||
let name = name.rsplit([' ', ',', ';']).next().unwrap_or(name);
|
||||
let normalized = name.replace('-', "_");
|
||||
if normalized.ends_with("api_key")
|
||||
|| normalized.ends_with("token")
|
||||
|| normalized.ends_with("secret")
|
||||
|| normalized.ends_with("password")
|
||||
|| normalized.ends_with("passwd")
|
||||
{
|
||||
return "redacted-credential".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Common credential token prefixes. These are intentionally checked at
|
||||
// word boundaries so model ids containing an incidental "sk" survive.
|
||||
let credential_prefix = lower
|
||||
.split(|ch: char| ch.is_whitespace() || matches!(ch, '=' | ':' | ',' | ';' | '"' | '\''))
|
||||
.filter(|part| !part.is_empty())
|
||||
.any(|part| {
|
||||
[
|
||||
"sk-",
|
||||
"sk_",
|
||||
"rk-",
|
||||
"pk-",
|
||||
"ghp_",
|
||||
"gho_",
|
||||
"ghu_",
|
||||
"ghs_",
|
||||
"github_pat_",
|
||||
"hf_",
|
||||
"glpat-",
|
||||
"xoxb-",
|
||||
"xoxp-",
|
||||
"xoxa-",
|
||||
"akia",
|
||||
"aiza",
|
||||
"eyj",
|
||||
]
|
||||
.iter()
|
||||
.any(|prefix| part.starts_with(prefix))
|
||||
});
|
||||
if credential_prefix {
|
||||
return "redacted-credential".to_string();
|
||||
}
|
||||
|
||||
let windows_absolute = value.as_bytes().get(1) == Some(&b':')
|
||||
&& value
|
||||
.as_bytes()
|
||||
.get(2)
|
||||
.is_some_and(|separator| matches!(separator, b'/' | b'\\'));
|
||||
let contains_local_root = [
|
||||
"/users/",
|
||||
"/volumes/",
|
||||
"/home/",
|
||||
"/private/",
|
||||
"\\users\\",
|
||||
"file://",
|
||||
"/.ssh/",
|
||||
"\\.ssh\\",
|
||||
]
|
||||
.iter()
|
||||
.any(|needle| lower.contains(needle));
|
||||
let looks_like_relative_path = value.contains('\\')
|
||||
|| lower.starts_with(".ssh/")
|
||||
|| lower.starts_with(".ssh\\")
|
||||
|| lower.split('/').any(|segment| {
|
||||
matches!(
|
||||
segment,
|
||||
"." | ".."
|
||||
| ".ssh"
|
||||
| ".config"
|
||||
| "secrets"
|
||||
| "secret"
|
||||
| "credentials"
|
||||
| "credential"
|
||||
| "relative"
|
||||
| "workspace"
|
||||
| "tmp"
|
||||
)
|
||||
});
|
||||
if std::path::Path::new(value).is_absolute()
|
||||
|| windows_absolute
|
||||
|| value.starts_with("~/")
|
||||
|| value.starts_with("./")
|
||||
|| value.starts_with("../")
|
||||
|| contains_local_root
|
||||
|| looks_like_relative_path
|
||||
{
|
||||
return "redacted-local-path".to_string();
|
||||
}
|
||||
let bounded: String = value
|
||||
.chars()
|
||||
.filter(|ch| !ch.is_control())
|
||||
.take(MAX_PERSISTED_ROUTE_LABEL_CHARS)
|
||||
.collect();
|
||||
if bounded.is_empty() {
|
||||
"-".to_string()
|
||||
} else {
|
||||
bounded
|
||||
}
|
||||
}
|
||||
@@ -493,6 +493,15 @@ pub fn read_shell_sentinel(path: &Path) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Build a syntactically valid, non-secret JWT fixture without embedding a
|
||||
/// high-entropy token-shaped literal in Git history.
|
||||
pub fn future_test_jwt(label: &str) -> String {
|
||||
use base64::Engine as _;
|
||||
|
||||
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(br#"{"exp":9999999999}"#);
|
||||
format!("test.{payload}.{label}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -550,12 +559,3 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a syntactically valid, non-secret JWT fixture without embedding a
|
||||
/// high-entropy token-shaped literal in Git history.
|
||||
pub fn future_test_jwt(label: &str) -> String {
|
||||
use base64::Engine as _;
|
||||
|
||||
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(br#"{"exp":9999999999}"#);
|
||||
format!("test.{payload}.{label}")
|
||||
}
|
||||
@@ -425,7 +425,7 @@ pub fn with_test_active_denylist<R>(list: ReadDenylist, callback: impl FnOnce()
|
||||
*ACTIVE.write().unwrap() = self.0.take();
|
||||
}
|
||||
}
|
||||
let previous = std::mem::replace(&mut *ACTIVE.write().unwrap(), Some(Arc::new(list)));
|
||||
let previous = (*ACTIVE.write().unwrap()).replace(Arc::new(list));
|
||||
let _restore = RestoreActive(previous);
|
||||
callback()
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **MCP catalogs stay fresh:** a server's tools, resources, prompts and skills are rediscovered on their own connection when an advertised `ttlMs` expires or the server sends a list-changed notice. A tool resolved against the old catalog is refused with "retry the call", a removed tool cannot run, and a failed refresh reconnects instead of serving stale entries. Catalogs are never shared across servers, credentials or reconnects. (CW-7547)
|
||||
- **MCP Skills extension:** servers that advertise the published Skills extension have their skills listed in `list_mcp_resources`. Listing never activates a skill, and reading a skill file is refused unless its size and sha256 digest match the listing. MCP skills do not yet join `/skills`. (CW-7545)
|
||||
- **Computer Use is installed, not bundled:** the Computer Use plugin is no longer vendored into the binary. Install it from the `codewhale` marketplace, which points at its independent release; its consent tools load on first use, and decisions still require a human approval card. (CW-6732)
|
||||
- **First-party marketplace receipt:** the shipped catalog is pinned to marketplace revision `a5f1d210372ff48c9a278a0a39737956ac0f2d93`.
|
||||
- **Tool image validation:** an image returned by a tool must decode before it is sent, so truncated or corrupt files are omitted with a receipt even when they are already within the delivery budget.
|
||||
|
||||
- **Image-heavy session recovery:** ordinary image reads use a 256 KiB delivery budget. Requests measure serialized bytes, omit older images from an outbound copy with receipts, and retry an initial HTTP 413 once only with a smaller body. Saved images and executed tool actions remain intact. (CW-7550)
|
||||
@@ -210,6 +211,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
is now interrupted instead of running on with nothing able to stop it
|
||||
(#6558).
|
||||
|
||||
- Multiline mode now has a send key on every terminal: with
|
||||
`composer_multiline_mode = true`, `Enter` inserts a newline and `Alt-Enter`
|
||||
sends. `Shift-Enter` also sends where the terminal reports it distinctly; on
|
||||
Linux terminals that report it as plain `Enter`, multiline mode previously
|
||||
had no way to send. `Ctrl-J` always inserts a newline. Thanks
|
||||
[@luestr](https://github.com/luestr) for the report. (#7093)
|
||||
- Session saves no longer fail with "Fleet artifact path must stay within the
|
||||
workspace" when the Codewhale state root is a symlink or Windows junction to
|
||||
another volume: the state root is resolved before session-side state is
|
||||
opened. Thanks [@SparkofSpike](https://github.com/SparkofSpike). (#7097)
|
||||
- README translations carry refreshed source stamps after the 0.10.3 batch.
|
||||
Thanks [@Aboysky](https://github.com/Aboysky). (#7104)
|
||||
|
||||
### Contributors
|
||||
|
||||
- **[@7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — per-scope instruction budgets with visible truncation ([#7086](https://github.com/codewhale-hq/Codewhale/pull/7086), [#6526](https://github.com/codewhale-hq/Codewhale/issues/6526)).
|
||||
- **[@Lstarsky0](https://github.com/Lstarsky0)** — kept the `/diff` pager's place across a resize ([#7087](https://github.com/codewhale-hq/Codewhale/pull/7087)), said "Goal cleared." only when there was a goal ([#7089](https://github.com/codewhale-hq/Codewhale/pull/7089)), and stopped printing an unpriced session cost as `$0.0000` in `/status` ([#7090](https://github.com/codewhale-hq/Codewhale/pull/7090)).
|
||||
- **[@gaord](https://github.com/gaord)** — a running turn's cost and context in the Runtime ([#7074](https://github.com/codewhale-hq/Codewhale/pull/7074)).
|
||||
- **[Sh1Zuku / @SparkofSpike](https://github.com/SparkofSpike)** — resolved a relocated state root before session-side state opens ([#7097](https://github.com/codewhale-hq/Codewhale/pull/7097)).
|
||||
- **[Boyce / @Aboysky](https://github.com/Aboysky)** — refreshed the README translation source stamps ([#7104](https://github.com/codewhale-hq/Codewhale/pull/7104)).
|
||||
- **[@M-Maciej](https://github.com/M-Maciej)** — reported answerless output-limit stops in long Fleet proofs ([#6990](https://github.com/codewhale-hq/Codewhale/issues/6990)).
|
||||
- **[@asto18089](https://github.com/asto18089)** — diagnosed the NoNewPrivs child-matrix failure on hardened containers ([#7021](https://github.com/codewhale-hq/Codewhale/issues/7021)).
|
||||
- **[@luestr](https://github.com/luestr)** — reported the Linux multiline send key ([#7093](https://github.com/codewhale-hq/Codewhale/issues/7093)) and session export guidance ([#6996](https://github.com/codewhale-hq/Codewhale/issues/6996)).
|
||||
|
||||
## [0.10.2] - 2026-10-22
|
||||
|
||||
Codewhale v0.10.2 adds an animated pet view (`/pet on`), a live Terminal work
|
||||
|
||||
@@ -111,7 +111,7 @@ pub(crate) struct RemoteControlInferencePermit {
|
||||
}
|
||||
|
||||
#[cfg(any(test, all(feature = "test-thread-scoped-state", debug_assertions)))]
|
||||
#[cfg_attr(windows, allow(dead_code))]
|
||||
#[cfg_attr(all(not(test), debug_assertions), expect(dead_code))]
|
||||
pub(crate) async fn acquire_runtime_chat_inference_ownership() -> RuntimeChatInferenceOwnership {
|
||||
let gate = runtime_chat_inference_gate().write_owned().await;
|
||||
RuntimeChatInferenceOwnership { _gate: gate }
|
||||
|
||||
@@ -1429,7 +1429,7 @@ impl CodewhaleClient {
|
||||
// sanitizer, dump the offending indices so we can diagnose where
|
||||
// they came from on the next failure.
|
||||
if format!("{error:#}").contains("reasoning_content") {
|
||||
log_thinking_mode_violations(&body);
|
||||
log_thinking_mode_violations(body);
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ use codewhale_config::catalog::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[cfg_attr(windows, allow(dead_code))]
|
||||
#[cfg_attr(
|
||||
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
|
||||
expect(dead_code)
|
||||
)]
|
||||
pub(crate) const TAGS_PROBE_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
/// Upper bound on `/api/show` lookups per probe. A developer box can hold
|
||||
@@ -208,7 +211,10 @@ pub(crate) fn ollama_catalog_delta(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(windows, allow(dead_code))]
|
||||
#[cfg_attr(
|
||||
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
|
||||
expect(dead_code)
|
||||
)]
|
||||
pub(crate) fn probe_client() -> anyhow::Result<reqwest::Client> {
|
||||
// The first-run probe can run before any provider client has installed
|
||||
// the rustls crypto provider; the shared builder installs it (the bare
|
||||
@@ -218,7 +224,10 @@ pub(crate) fn probe_client() -> anyhow::Result<reqwest::Client> {
|
||||
.build()?)
|
||||
}
|
||||
|
||||
#[cfg_attr(windows, allow(dead_code))]
|
||||
#[cfg_attr(
|
||||
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
|
||||
expect(dead_code)
|
||||
)]
|
||||
pub(crate) async fn fetch_text(url: &str) -> anyhow::Result<String> {
|
||||
let response = probe_client()?.get(url).send().await?;
|
||||
if !response.status().is_success() {
|
||||
@@ -229,7 +238,10 @@ pub(crate) async fn fetch_text(url: &str) -> anyhow::Result<String> {
|
||||
.map_err(|reason| anyhow::anyhow!("Ollama catalog response rejected: {reason:?}"))
|
||||
}
|
||||
|
||||
#[cfg_attr(windows, allow(dead_code))]
|
||||
#[cfg_attr(
|
||||
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
|
||||
expect(dead_code)
|
||||
)]
|
||||
pub(crate) async fn fetch_tag_profile(origin: &str, tag: &str) -> anyhow::Result<OllamaTagProfile> {
|
||||
let response = probe_client()?
|
||||
.post(format!("{origin}/api/show"))
|
||||
|
||||
@@ -3802,7 +3802,7 @@ impl Engine {
|
||||
controls.active = control.clone();
|
||||
control
|
||||
};
|
||||
Box::pin(self.handle_message_operation(turn, Some(request))).await;
|
||||
Box::pin(self.handle_message_operation(*turn, Some(request))).await;
|
||||
}
|
||||
Op::SendMessage(spec) => {
|
||||
self.admitted_turn_control = {
|
||||
|
||||
@@ -1152,7 +1152,7 @@ async fn user_steer_during_parent_answer_still_gets_a_reply_with_healthy_childre
|
||||
.expect("steering must preempt an indefinitely silent request");
|
||||
assert_eq!(status, TurnOutcomeStatus::Completed, "{phase}: {error:?}");
|
||||
assert_eq!(client.calls.load(Ordering::SeqCst), 3, "{phase}");
|
||||
let requests = client.requests.lock().unwrap();
|
||||
let requests = client.requests.lock().unwrap().clone();
|
||||
assert!(requests[1].messages.iter().any(|message| message.content.iter().any(|block| {
|
||||
matches!(block, ContentBlock::Text { text, .. } if text.contains("Also read state.txt and include its evidence."))
|
||||
})), "{phase}");
|
||||
@@ -1179,7 +1179,6 @@ async fn user_steer_during_parent_answer_still_gets_a_reply_with_healthy_childre
|
||||
requests[0].system, requests[1].system,
|
||||
"{phase}: pinned system prefix changed"
|
||||
);
|
||||
drop(requests);
|
||||
let mut events = handle.rx_event.write().await;
|
||||
let events = std::iter::from_fn(|| events.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(events.iter().all(|event| !matches!(event, Event::ToolCallStarted { model_call: Some(call), .. } if call.provider_id == "call-must-not-run")), "{phase}");
|
||||
|
||||
@@ -240,7 +240,7 @@ pub struct TurnSpec {
|
||||
#[derive(Debug)]
|
||||
pub enum Op {
|
||||
Review {
|
||||
turn: TurnSpec,
|
||||
turn: Box<TurnSpec>,
|
||||
request: codewhale_protocol::op::ReviewOperation,
|
||||
},
|
||||
/// Send a message to the AI
|
||||
|
||||
@@ -1103,7 +1103,7 @@ pub fn event_to_protocol(event: &Event, ids: &ProtocolIds) -> wire::EventMsg {
|
||||
pub fn op_to_protocol(op: &Op) -> wire_op::Op {
|
||||
match op {
|
||||
Op::Review { turn, request } => wire_op::Op::Review {
|
||||
turn: turn_spec_to_wire(turn),
|
||||
turn: Box::new(turn_spec_to_wire(turn)),
|
||||
request: request.clone(),
|
||||
},
|
||||
Op::SendMessage(spec) => wire_op::Op::SendMessage(turn_spec_to_wire(spec)),
|
||||
|
||||
@@ -91,7 +91,7 @@ fn request(
|
||||
};
|
||||
let frame = json!({"jsonrpc":"2.0","id":id,"method":"proc/write","params":params});
|
||||
match parse_host_message(frame, HostTier::Builtin).unwrap() {
|
||||
HostMessage::Request { request, .. } => request,
|
||||
HostMessage::Request { request, .. } => *request,
|
||||
_ => panic!("decoded process write must be a request"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,7 +782,7 @@ pub enum HostNotification {
|
||||
pub enum HostMessage {
|
||||
Request {
|
||||
id: u64,
|
||||
request: HostRequest,
|
||||
request: Box<HostRequest>,
|
||||
},
|
||||
Notification(HostNotification),
|
||||
Response {
|
||||
@@ -864,64 +864,64 @@ pub fn parse_host_message(value: Value, tier: HostTier) -> Result<HostMessage, P
|
||||
.map_err(|reason| perr(format!("{method}: {reason}")))?;
|
||||
HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::Register(register),
|
||||
request: Box::new(HostRequest::Register(register)),
|
||||
}
|
||||
}
|
||||
("registry/unregister", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::Unregister(params(&method, p)?),
|
||||
request: Box::new(HostRequest::Unregister(params(&method, p)?)),
|
||||
},
|
||||
("user-questions/ask", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::UserQuestion(params(&method, p)?),
|
||||
request: Box::new(HostRequest::UserQuestion(params(&method, p)?)),
|
||||
},
|
||||
("session/checkpoint", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::SessionCheckpoint(params(&method, p)?),
|
||||
request: Box::new(HostRequest::SessionCheckpoint(params(&method, p)?)),
|
||||
},
|
||||
("core/call", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::CoreCall(params(&method, p)?),
|
||||
request: Box::new(HostRequest::CoreCall(params(&method, p)?)),
|
||||
},
|
||||
("net/start", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::NetStart(params(&method, p)?),
|
||||
request: Box::new(HostRequest::NetStart(params(&method, p)?)),
|
||||
},
|
||||
("net/fetch", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::NetFetch(params(&method, p)?),
|
||||
request: Box::new(HostRequest::NetFetch(params(&method, p)?)),
|
||||
},
|
||||
("net/read", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::NetRead(params(&method, p)?),
|
||||
request: Box::new(HostRequest::NetRead(params(&method, p)?)),
|
||||
},
|
||||
("net/release", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::NetRelease(params(&method, p)?),
|
||||
request: Box::new(HostRequest::NetRelease(params(&method, p)?)),
|
||||
},
|
||||
("net/close", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::NetClose(params(&method, p)?),
|
||||
request: Box::new(HostRequest::NetClose(params(&method, p)?)),
|
||||
},
|
||||
("proc/close", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::ProcClose(params(&method, p)?),
|
||||
request: Box::new(HostRequest::ProcClose(params(&method, p)?)),
|
||||
},
|
||||
("proc/write", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::ProcWrite(params(&method, p)?),
|
||||
request: Box::new(HostRequest::ProcWrite(params(&method, p)?)),
|
||||
},
|
||||
("proc/read", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::ProcRead(params(&method, p)?),
|
||||
request: Box::new(HostRequest::ProcRead(params(&method, p)?)),
|
||||
},
|
||||
("exec/redeem", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::ExecutionRedeem(params(&method, p)?),
|
||||
request: Box::new(HostRequest::ExecutionRedeem(params(&method, p)?)),
|
||||
},
|
||||
("proc/launch", Some(id)) => HostMessage::Request {
|
||||
id,
|
||||
request: HostRequest::ProcLaunch(params(&method, p)?),
|
||||
request: Box::new(HostRequest::ProcLaunch(params(&method, p)?)),
|
||||
},
|
||||
("host/hello", None) => {
|
||||
let hello: HelloParams = params(&method, p)?;
|
||||
@@ -974,7 +974,7 @@ impl HostMessage {
|
||||
#[must_use]
|
||||
pub fn to_value(&self) -> Value {
|
||||
match self {
|
||||
Self::Request { id, request } => match request {
|
||||
Self::Request { id, request } => match request.as_ref() {
|
||||
HostRequest::Register(p) => request_value(*id, "registry/register", to_value(p)),
|
||||
HostRequest::Unregister(p) => {
|
||||
request_value(*id, "registry/unregister", to_value(p))
|
||||
|
||||
@@ -808,11 +808,10 @@ fn production_methods_follow_their_tier_rows_and_reserved_families_stay_builtin_
|
||||
for tier in HostTier::ALL {
|
||||
let parsed = parse_host_message(frame.clone(), tier).unwrap();
|
||||
assert_eq!(parsed.to_value(), frame);
|
||||
let HostMessage::Request {
|
||||
request: HostRequest::UserQuestion(params),
|
||||
..
|
||||
} = parsed
|
||||
else {
|
||||
let HostMessage::Request { request, .. } = parsed else {
|
||||
panic!("wrong question variant")
|
||||
};
|
||||
let HostRequest::UserQuestion(params) = *request else {
|
||||
panic!("wrong question variant")
|
||||
};
|
||||
assert_eq!(params.questions[0].options.len(), count);
|
||||
|
||||
@@ -165,6 +165,7 @@ pub(crate) struct HostLaunch {
|
||||
/// build pins ([`tier::BUILTIN_MODULES`]), which the host bundle embedded
|
||||
/// from the same build must agree with ([`check_hello_identity`]).
|
||||
pub builtin_modules: &'static [BuiltinModule],
|
||||
#[cfg(not(windows))]
|
||||
pub native_snapshot_roots: Vec<PathBuf>,
|
||||
/// Exact verified Native LPAC plan; Builtin retains its labelled exception.
|
||||
#[cfg(windows)]
|
||||
@@ -551,7 +552,6 @@ pub(crate) fn plan_launch(
|
||||
memory_cap,
|
||||
memory: MemoryEnforcement::JobObject,
|
||||
builtin_modules: tier::BUILTIN_MODULES,
|
||||
native_snapshot_roots: Vec::new(),
|
||||
windows: Some(sandbox),
|
||||
});
|
||||
}
|
||||
@@ -569,8 +569,12 @@ pub(crate) fn plan_launch(
|
||||
bundle,
|
||||
&snapshot_roots,
|
||||
);
|
||||
let mut launch = host_launch(tier, runtime, args, data, memory_cap, wrapped)?;
|
||||
launch.native_snapshot_roots = snapshot_roots;
|
||||
let launch = host_launch(tier, runtime, args, data, memory_cap, wrapped)?;
|
||||
#[cfg(not(windows))]
|
||||
let launch = HostLaunch {
|
||||
native_snapshot_roots: snapshot_roots,
|
||||
..launch
|
||||
};
|
||||
Ok(launch)
|
||||
}
|
||||
|
||||
@@ -810,15 +814,15 @@ fn native_readable_paths(
|
||||
return Err("Native writable data overlaps a runtime or package read grant".into());
|
||||
}
|
||||
canonical.push(resolved);
|
||||
if path.is_file() {
|
||||
if let (Some(parent), Some(name)) = (path.parent(), path.file_name()) {
|
||||
canonical.push(
|
||||
parent
|
||||
.canonicalize()
|
||||
.map_err(|error| error.to_string())?
|
||||
.join(name),
|
||||
);
|
||||
}
|
||||
if path.is_file()
|
||||
&& let (Some(parent), Some(name)) = (path.parent(), path.file_name())
|
||||
{
|
||||
canonical.push(
|
||||
parent
|
||||
.canonicalize()
|
||||
.map_err(|error| error.to_string())?
|
||||
.join(name),
|
||||
);
|
||||
}
|
||||
}
|
||||
paths.extend(canonical);
|
||||
@@ -1095,6 +1099,7 @@ fn host_launch(
|
||||
memory_cap,
|
||||
memory: MemoryEnforcement::planned(runtime.kind),
|
||||
builtin_modules: tier::BUILTIN_MODULES,
|
||||
#[cfg(not(windows))]
|
||||
native_snapshot_roots: Vec::new(),
|
||||
#[cfg(windows)]
|
||||
windows: None,
|
||||
@@ -1556,7 +1561,7 @@ pub(crate) struct HostProcess {
|
||||
/// How the cap is enforced for this process, settled at the handshake.
|
||||
memory: Arc<std::sync::OnceLock<MemoryEnforcement>>,
|
||||
pub sandbox: HostSandbox,
|
||||
#[cfg_attr(windows, expect(dead_code))]
|
||||
#[cfg(not(windows))]
|
||||
native_snapshot_roots: Vec<PathBuf>,
|
||||
tree: Arc<codewhale_runtime::process_tree::ProcessTree>,
|
||||
#[cfg(windows)]
|
||||
@@ -1732,7 +1737,7 @@ impl HostProcess {
|
||||
if !snapshot_roots.iter().any(|grant| canonical.starts_with(grant) && canonical != *grant) {
|
||||
return Err("Native snapshot store was not granted to this host; restart Codewhale to admit this store".into());
|
||||
}
|
||||
let current = native_snapshot_roots(&[authority.state_path.clone()])?;
|
||||
let current = native_snapshot_roots(std::slice::from_ref(&authority.state_path))?;
|
||||
if !current.iter().any(|grant| snapshot_roots.contains(grant) && canonical.starts_with(grant)) {
|
||||
return Err("Native snapshot store changed after host launch".into());
|
||||
}
|
||||
@@ -1941,6 +1946,7 @@ impl HostProcess {
|
||||
memory_cap: launch.memory_cap,
|
||||
memory,
|
||||
sandbox: launch.sandbox.clone(),
|
||||
#[cfg(not(windows))]
|
||||
native_snapshot_roots: launch.native_snapshot_roots.clone(),
|
||||
tree,
|
||||
#[cfg(windows)]
|
||||
@@ -2396,7 +2402,7 @@ impl Reader {
|
||||
};
|
||||
let _ = call.tx.send(result);
|
||||
}
|
||||
HostMessage::Request { id, request } => self.start_host_request(id, request)?,
|
||||
HostMessage::Request { id, request } => self.start_host_request(id, *request)?,
|
||||
HostMessage::Notification(notification) => match notification {
|
||||
HostNotification::Hello(hello) => {
|
||||
if let Some(tx) = self.handshake.lock().expect("handshake lock").hello.take() {
|
||||
@@ -3012,7 +3018,7 @@ mod tests {
|
||||
fn request(rig: &Rig, id: u64, plugin: &str) -> Result<(), String> {
|
||||
rig.reader.handle(HostMessage::Request {
|
||||
id,
|
||||
request: register_request(plugin),
|
||||
request: Box::new(register_request(plugin)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3060,10 +3066,10 @@ mod tests {
|
||||
rig.reader
|
||||
.handle(HostMessage::Request {
|
||||
id: 6,
|
||||
request: HostRequest::Unregister(protocol::UnregisterParams {
|
||||
request: Box::new(HostRequest::Unregister(protocol::UnregisterParams {
|
||||
owner: owner("p"),
|
||||
handle: 9,
|
||||
}),
|
||||
})),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
||||
@@ -653,7 +653,7 @@ condition = { type = "exit_code", code = 1 }
|
||||
problem.event.is_none(),
|
||||
"the override is not one hook's problem: {problem:?}"
|
||||
);
|
||||
assert!(problem_summary(&problem).contains("`[hooks]` setting"));
|
||||
assert!(problem_summary(problem).contains("`[hooks]` setting"));
|
||||
|
||||
// Even unvalidated, the accessors refuse the value rather than hand a
|
||||
// zero budget to the executor.
|
||||
|
||||
+59
-55
@@ -1553,6 +1553,48 @@ fn validate_serve_mode_selection(
|
||||
Ok(http_selected)
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
struct McpAddArgs {
|
||||
/// Server name
|
||||
name: String,
|
||||
/// Command to launch stdio server
|
||||
#[arg(long, conflicts_with = "url")]
|
||||
command: Option<String>,
|
||||
/// URL for streamable HTTP/SSE server
|
||||
#[arg(long, conflicts_with = "command")]
|
||||
url: Option<String>,
|
||||
/// Explicit URL transport override. Use "sse" for legacy SSE endpoints.
|
||||
#[arg(long, requires = "url")]
|
||||
transport: Option<String>,
|
||||
/// Environment variable containing a bearer token for URL-based servers
|
||||
#[arg(long, requires = "url")]
|
||||
bearer_token_env_var: Option<String>,
|
||||
/// Pre-registered OAuth client ID, bound to --oauth-issuer
|
||||
#[arg(long, requires_all = ["url", "oauth_issuer"])]
|
||||
oauth_client_id: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
requires = "url",
|
||||
help = "Exact authorization issuer bound to this OAuth client"
|
||||
)]
|
||||
oauth_issuer: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
requires = "url",
|
||||
help = "HTTPS URL of a hosted OAuth client metadata document"
|
||||
)]
|
||||
oauth_client_metadata_url: Option<String>,
|
||||
/// Exact OAuth resource pin checked against server discovery
|
||||
#[arg(long, requires = "url")]
|
||||
oauth_resource: Option<String>,
|
||||
/// OAuth scope to request during login. Repeat or comma-separate.
|
||||
#[arg(long = "scope", requires = "url", value_delimiter = ',')]
|
||||
scopes: Vec<String>,
|
||||
/// Arguments for command-based servers
|
||||
#[arg(long = "arg", allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
enum McpCommand {
|
||||
/// List configured MCP servers
|
||||
@@ -1577,46 +1619,7 @@ enum McpCommand {
|
||||
server: Option<String>,
|
||||
},
|
||||
/// Add an MCP server entry
|
||||
Add {
|
||||
/// Server name
|
||||
name: String,
|
||||
/// Command to launch stdio server
|
||||
#[arg(long, conflicts_with = "url")]
|
||||
command: Option<String>,
|
||||
/// URL for streamable HTTP/SSE server
|
||||
#[arg(long, conflicts_with = "command")]
|
||||
url: Option<String>,
|
||||
/// Explicit URL transport override. Use "sse" for legacy SSE endpoints.
|
||||
#[arg(long, requires = "url")]
|
||||
transport: Option<String>,
|
||||
/// Environment variable containing a bearer token for URL-based servers
|
||||
#[arg(long, requires = "url")]
|
||||
bearer_token_env_var: Option<String>,
|
||||
/// Pre-registered OAuth client ID, bound to --oauth-issuer
|
||||
#[arg(long, requires_all = ["url", "oauth_issuer"])]
|
||||
oauth_client_id: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
requires = "url",
|
||||
help = "Exact authorization issuer bound to this OAuth client"
|
||||
)]
|
||||
oauth_issuer: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
requires = "url",
|
||||
help = "HTTPS URL of a hosted OAuth client metadata document"
|
||||
)]
|
||||
oauth_client_metadata_url: Option<String>,
|
||||
/// Exact OAuth resource pin checked against server discovery
|
||||
#[arg(long, requires = "url")]
|
||||
oauth_resource: Option<String>,
|
||||
/// OAuth scope to request during login. Repeat or comma-separate.
|
||||
#[arg(long = "scope", requires = "url", value_delimiter = ',')]
|
||||
scopes: Vec<String>,
|
||||
/// Arguments for command-based servers
|
||||
#[arg(long = "arg", allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
Add(Box<McpAddArgs>),
|
||||
/// Authenticate to a URL-based MCP server using OAuth
|
||||
Login {
|
||||
/// Server name
|
||||
@@ -12224,19 +12227,20 @@ async fn run_mcp_command(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
McpCommand::Add {
|
||||
name,
|
||||
command,
|
||||
url,
|
||||
transport,
|
||||
bearer_token_env_var,
|
||||
oauth_client_id,
|
||||
oauth_issuer,
|
||||
oauth_client_metadata_url,
|
||||
oauth_resource,
|
||||
scopes,
|
||||
args,
|
||||
} => {
|
||||
McpCommand::Add(args) => {
|
||||
let McpAddArgs {
|
||||
name,
|
||||
command,
|
||||
url,
|
||||
transport,
|
||||
bearer_token_env_var,
|
||||
oauth_client_id,
|
||||
oauth_issuer,
|
||||
oauth_client_metadata_url,
|
||||
oauth_resource,
|
||||
scopes,
|
||||
args,
|
||||
} = *args;
|
||||
if command.is_none() && url.is_none() {
|
||||
bail!("Provide either --command or --url for `mcp add`.");
|
||||
}
|
||||
@@ -20932,10 +20936,10 @@ mod mcp_add_arg_tests {
|
||||
let Some(Commands::Mcp { command }) = cli.command else {
|
||||
panic!("expected mcp command");
|
||||
};
|
||||
let McpCommand::Add { args, .. } = command else {
|
||||
let McpCommand::Add(args) = command else {
|
||||
panic!("expected mcp add subcommand");
|
||||
};
|
||||
assert_eq!(args, vec!["-y".to_string()]);
|
||||
assert_eq!(args.args, vec!["-y".to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,10 @@ pub(crate) fn should_adopt_live_local_ollama(
|
||||
}
|
||||
|
||||
/// Resolve the OpenAI-compat Ollama base URL (`…/v1`) from config defaults.
|
||||
#[cfg_attr(windows, allow(dead_code))]
|
||||
#[cfg_attr(
|
||||
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
|
||||
expect(dead_code)
|
||||
)]
|
||||
pub(crate) fn ollama_v1_base_url(config: &Config) -> String {
|
||||
config
|
||||
.resolve_provider_pin_identity("ollama")
|
||||
@@ -69,7 +72,10 @@ pub(crate) fn ollama_v1_base_url(config: &Config) -> String {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg_attr(windows, allow(dead_code))]
|
||||
#[cfg_attr(
|
||||
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
|
||||
expect(dead_code)
|
||||
)]
|
||||
fn record_ollama_tags_into_lake(
|
||||
endpoint_v1: &str,
|
||||
tags: &[String],
|
||||
@@ -100,7 +106,10 @@ fn record_ollama_tags_into_lake(
|
||||
/// Probe local Ollama for a live catalog. Prefers native `/api/tags`, falls
|
||||
/// back to OpenAI-compat `/v1/models`. Returns `None` when nothing useful
|
||||
/// answered — never invents a tag.
|
||||
#[cfg_attr(windows, allow(dead_code))]
|
||||
#[cfg_attr(
|
||||
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
|
||||
expect(dead_code)
|
||||
)]
|
||||
pub(crate) async fn probe_live_local_ollama_catalog(
|
||||
config: &Config,
|
||||
) -> Option<LiveLocalOllamaCatalog> {
|
||||
|
||||
@@ -1902,8 +1902,7 @@ fn load_rules_from_dir(
|
||||
}
|
||||
|
||||
/// User-owned global instructions may intentionally resolve through links.
|
||||
#[cfg(test)]
|
||||
#[cfg_attr(windows, expect(dead_code))]
|
||||
#[cfg(all(test, unix))]
|
||||
fn load_global_context_file(path: &Path) -> Result<String, ProjectContextError> {
|
||||
load_global_context_file_limited(path, DEFAULT_FILE_MAX_BYTES)
|
||||
}
|
||||
|
||||
@@ -1556,6 +1556,15 @@ pub(crate) fn measure_static_prefix(system: Option<&SystemPrompt>, tools: &[Tool
|
||||
|
||||
const GUARD_PASSES_PER_SCOPE: usize = 2;
|
||||
|
||||
type GuardInstructionResult = Result<
|
||||
(
|
||||
Option<SystemPrompt>,
|
||||
Option<crate::project_context::InstructionAssembly>,
|
||||
Vec<crate::project_context::InstructionNotice>,
|
||||
),
|
||||
crate::project_context::InstructionRefusal,
|
||||
>;
|
||||
|
||||
fn replace_installed_instruction_block(
|
||||
prompt: &mut Option<SystemPrompt>,
|
||||
raw: &crate::project_context::ProjectContext,
|
||||
@@ -1598,14 +1607,7 @@ pub(crate) fn guard_instruction_prefix(
|
||||
mut assembly: Option<crate::project_context::InstructionAssembly>,
|
||||
budgets: crate::project_context::InstructionBudgets,
|
||||
tools: &[Tool],
|
||||
) -> Result<
|
||||
(
|
||||
Option<SystemPrompt>,
|
||||
Option<crate::project_context::InstructionAssembly>,
|
||||
Vec<crate::project_context::InstructionNotice>,
|
||||
),
|
||||
crate::project_context::InstructionRefusal,
|
||||
> {
|
||||
) -> GuardInstructionResult {
|
||||
use crate::project_context::{
|
||||
InstructionAction, InstructionBudgetPolicy, InstructionLimit, InstructionNotice,
|
||||
InstructionRefusal, InstructionScope,
|
||||
|
||||
+189
-189
@@ -524,6 +524,195 @@ pub fn unset(path: &Path, key: &str) -> Result<()> {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn set_provider_model_document(
|
||||
doc: &mut toml_edit::DocumentMut,
|
||||
identity: &ProviderIdentity,
|
||||
model: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut config =
|
||||
codewhale_runtime::config::parse_config_base(&doc.to_string()).map_err(|_| {
|
||||
anyhow::anyhow!("Could not parse destination route identity; contents omitted")
|
||||
})?;
|
||||
crate::plugins::providers::apply_startup_providers(&mut config)?;
|
||||
persistence::set_verified_provider_model_document(doc, &config, identity, model)
|
||||
}
|
||||
|
||||
/// One persistent owner and atomic write for an explicitly saved route.
|
||||
///
|
||||
/// Also returns the undo for exactly this write, taken under the config lock,
|
||||
/// so a caller whose follow-up apply step is rejected can take back only the
|
||||
/// switch.
|
||||
pub(crate) fn persist_provider_selection(
|
||||
config_path: Option<&Path>,
|
||||
identity: &ProviderIdentity,
|
||||
model: Option<&str>,
|
||||
) -> anyhow::Result<(std::path::PathBuf, codewhale_config::ConfigDocumentUndo)> {
|
||||
let path = persistence::config_toml_path(config_path)?;
|
||||
let ((), undo) =
|
||||
codewhale_config::mutate_config_document_undoable_with_migration(&path, |doc, moved| {
|
||||
persistence::migrate_legacy_route_preferences(&path, doc)?;
|
||||
let mut config = codewhale_runtime::config::parse_config_after_locked_migration(
|
||||
&doc.to_string(),
|
||||
moved,
|
||||
)
|
||||
.map_err(|_| anyhow::anyhow!("Could not parse destination route; contents omitted"))?;
|
||||
crate::plugins::providers::apply_startup_providers(&mut config)?;
|
||||
config
|
||||
.verify_provider_identity(identity)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
if let Some(model) = model {
|
||||
persistence::set_verified_provider_model_document(doc, &config, identity, model)?;
|
||||
}
|
||||
persistence::set_document_value(
|
||||
doc,
|
||||
&["provider"],
|
||||
identity.persisted_id().unwrap_or(identity.key.as_str()),
|
||||
)?;
|
||||
reconcile_root_model_aliases(doc, &config, identity)
|
||||
})?;
|
||||
Ok((path, undo))
|
||||
}
|
||||
|
||||
/// Keep a root `default_text_model` alias from stranding a route switch,
|
||||
/// without discarding the choice it holds.
|
||||
///
|
||||
/// The root alias is the *active* route's fallback: `Config` resolves it only
|
||||
/// when the selected route has no model of its own. A switch that leaves the
|
||||
/// incoming route without a leaf therefore hands it an alias naming the route
|
||||
/// on its way out, and `Config::load` rejects the file the caller just wrote —
|
||||
/// a committed switch that produces an unloadable config — or, on a
|
||||
/// pass-through incoming route that loads it, a model the incoming route never
|
||||
/// chose while the outgoing route comes back on its catalog default.
|
||||
///
|
||||
/// When the outgoing route has no leaf and was resolving the alias, relocate
|
||||
/// that value onto the outgoing route's own canonical leaf, because it is real
|
||||
/// saved state, and only then clear the alias. Any other alias the incoming
|
||||
/// route already shadows with its own leaf is inert and stays: deleting a
|
||||
/// saved choice to satisfy validation of a value nothing resolves is data
|
||||
/// loss, not a repair. Likewise an unnamed custom route stores its
|
||||
/// model in the alias itself and has no leaf to receive it. If the incoming
|
||||
/// route cannot shadow that value, refuse the switch atomically and ask for an
|
||||
/// explicit destination model instead of saving an unloadable configuration.
|
||||
///
|
||||
/// `previous` is the document's configuration before the switch; `incoming` is
|
||||
/// the identity now selected. Every route writer calls this, so no writer can
|
||||
/// keep a private rule about which route owns the root alias.
|
||||
pub(crate) fn reconcile_root_model_aliases(
|
||||
doc: &mut toml_edit::DocumentMut,
|
||||
previous: &codewhale_runtime::config::Config,
|
||||
incoming: &codewhale_runtime::config::ProviderIdentity,
|
||||
) -> anyhow::Result<()> {
|
||||
// A freshly admitted id-less root keeps its legacy alias as a receipt.
|
||||
// Its effective model is written to the canonical leaf created by this
|
||||
// same-lock migration; the alias is never another route's saved choice.
|
||||
if incoming.provider == ProviderKind::Custom && incoming.persisted_id().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
// The outgoing route was resolving the alias as its own model. Move it
|
||||
// onto that route's leaf even when the incoming route could load it: a
|
||||
// pass-through incoming route would otherwise inherit a model it never
|
||||
// chose, and the outgoing route would come back on its catalog default.
|
||||
if let Some((outgoing, value)) = previous.root_model_alias_owned_by_outgoing(incoming) {
|
||||
persistence::set_verified_provider_model_document(doc, previous, &outgoing, &value)?;
|
||||
unset_root_model_aliases(doc)?;
|
||||
return Ok(());
|
||||
}
|
||||
// Below, only `default_text_model` is read. The legacy root `model` key is
|
||||
// never what blocks a load: `Config::default_model` already refuses to
|
||||
// route a foreign legacy value to a provider that cannot serve it, and
|
||||
// `Config::validate` does not consult it, so relocating it would move a
|
||||
// value nothing is asking about. It is still cleared together with the
|
||||
// alias that shadows it (`unset_root_model_aliases`).
|
||||
const ROOT_KEY: &str = "default_text_model";
|
||||
let Some(value) = doc
|
||||
.get(ROOT_KEY)
|
||||
.and_then(toml_edit::Item::as_str)
|
||||
.map(str::to_owned)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut switched = codewhale_runtime::config::parse_config_base(&doc.to_string())
|
||||
.map_err(|_| anyhow::anyhow!("Could not parse switched route; contents omitted"))?;
|
||||
crate::plugins::providers::apply_startup_providers(&mut switched)?;
|
||||
// `Config::validate` is the single authority on what the incoming route can
|
||||
// serve, so a writer cannot disagree with the loader. Act only when this
|
||||
// alias is what the loader rejects: a document already broken for an
|
||||
// unrelated reason is not this writer's to rewrite.
|
||||
let mut without_alias = switched.clone();
|
||||
without_alias.default_text_model = None;
|
||||
if switched
|
||||
.provider_config_for(incoming)
|
||||
.and_then(|entry| entry.model.as_deref())
|
||||
.is_some()
|
||||
|| switched.validate().is_ok()
|
||||
|| without_alias.validate().is_err()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
// An empty or control-bearing alias names no saved model. Nothing to
|
||||
// relocate, and clearing it loses nothing.
|
||||
if value.trim().is_empty() || value.chars().any(char::is_control) {
|
||||
unset_root_model_aliases(doc)?;
|
||||
return Ok(());
|
||||
}
|
||||
// No leaf can hold this value. Keep the only copy of the user's choice
|
||||
// rather than discard it for a document the incoming route loads as soon as
|
||||
// it saves a model of its own.
|
||||
let outgoing = previous.active_provider_identity().ok();
|
||||
if outgoing.as_ref().is_some_and(|outgoing| {
|
||||
outgoing != incoming
|
||||
&& outgoing.provider == ProviderKind::Custom
|
||||
&& outgoing.persisted_id().is_none()
|
||||
}) {
|
||||
// This same-lock canonicalizer copied the root model before moving
|
||||
// its endpoint. Clear an alias only when that exact outgoing leaf
|
||||
// proves the choice survives; otherwise retain the refusal.
|
||||
if outgoing.as_ref().is_some_and(|outgoing| {
|
||||
previous
|
||||
.provider_config_for(outgoing)
|
||||
.and_then(|entry| entry.model.as_deref())
|
||||
== Some(value.as_str())
|
||||
}) {
|
||||
return unset_root_model_aliases(doc);
|
||||
}
|
||||
anyhow::bail!(
|
||||
"Choose a model for the destination provider before switching from a legacy custom connection; the saved configuration was not changed"
|
||||
);
|
||||
}
|
||||
let Some(outgoing) = outgoing.as_ref().filter(|outgoing| *outgoing != incoming) else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut scoped = switched;
|
||||
scoped
|
||||
.scope_to_provider_identity(outgoing)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
if scoped
|
||||
.provider_config_for(outgoing)
|
||||
.and_then(|entry| entry.model.as_deref())
|
||||
.is_none()
|
||||
{
|
||||
persistence::set_verified_provider_model_document(doc, previous, outgoing, &value)?;
|
||||
}
|
||||
// The outgoing route either already saved its own choice or has just
|
||||
// received this one, so the alias is now a shadowed duplicate that only
|
||||
// blocks the incoming route.
|
||||
unset_root_model_aliases(doc)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the root model alias: `default_text_model` and the legacy root
|
||||
/// `model` that `Config::default_model` falls back to when the former is
|
||||
/// unset. While `default_text_model` is present the legacy key resolves on no
|
||||
/// route, so removing only the alias would resurrect a stale legacy model on
|
||||
/// the incoming route (a pass-through route would then use a model nobody
|
||||
/// chose for it). When the legacy key *is* the alias, the caller has already
|
||||
/// moved its value onto the outgoing route's leaf.
|
||||
fn unset_root_model_aliases(doc: &mut toml_edit::DocumentMut) -> anyhow::Result<()> {
|
||||
persistence::unset_document_value(doc, &["default_text_model"])?;
|
||||
persistence::unset_document_value(doc, &["model"])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -899,192 +1088,3 @@ model = "Other-X"
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_provider_model_document(
|
||||
doc: &mut toml_edit::DocumentMut,
|
||||
identity: &ProviderIdentity,
|
||||
model: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut config =
|
||||
codewhale_runtime::config::parse_config_base(&doc.to_string()).map_err(|_| {
|
||||
anyhow::anyhow!("Could not parse destination route identity; contents omitted")
|
||||
})?;
|
||||
crate::plugins::providers::apply_startup_providers(&mut config)?;
|
||||
persistence::set_verified_provider_model_document(doc, &config, identity, model)
|
||||
}
|
||||
|
||||
/// One persistent owner and atomic write for an explicitly saved route.
|
||||
///
|
||||
/// Also returns the undo for exactly this write, taken under the config lock,
|
||||
/// so a caller whose follow-up apply step is rejected can take back only the
|
||||
/// switch.
|
||||
pub(crate) fn persist_provider_selection(
|
||||
config_path: Option<&Path>,
|
||||
identity: &ProviderIdentity,
|
||||
model: Option<&str>,
|
||||
) -> anyhow::Result<(std::path::PathBuf, codewhale_config::ConfigDocumentUndo)> {
|
||||
let path = persistence::config_toml_path(config_path)?;
|
||||
let ((), undo) =
|
||||
codewhale_config::mutate_config_document_undoable_with_migration(&path, |doc, moved| {
|
||||
persistence::migrate_legacy_route_preferences(&path, doc)?;
|
||||
let mut config = codewhale_runtime::config::parse_config_after_locked_migration(
|
||||
&doc.to_string(),
|
||||
moved,
|
||||
)
|
||||
.map_err(|_| anyhow::anyhow!("Could not parse destination route; contents omitted"))?;
|
||||
crate::plugins::providers::apply_startup_providers(&mut config)?;
|
||||
config
|
||||
.verify_provider_identity(identity)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
if let Some(model) = model {
|
||||
persistence::set_verified_provider_model_document(doc, &config, identity, model)?;
|
||||
}
|
||||
persistence::set_document_value(
|
||||
doc,
|
||||
&["provider"],
|
||||
identity.persisted_id().unwrap_or(identity.key.as_str()),
|
||||
)?;
|
||||
reconcile_root_model_aliases(doc, &config, identity)
|
||||
})?;
|
||||
Ok((path, undo))
|
||||
}
|
||||
|
||||
/// Keep a root `default_text_model` alias from stranding a route switch,
|
||||
/// without discarding the choice it holds.
|
||||
///
|
||||
/// The root alias is the *active* route's fallback: `Config` resolves it only
|
||||
/// when the selected route has no model of its own. A switch that leaves the
|
||||
/// incoming route without a leaf therefore hands it an alias naming the route
|
||||
/// on its way out, and `Config::load` rejects the file the caller just wrote —
|
||||
/// a committed switch that produces an unloadable config — or, on a
|
||||
/// pass-through incoming route that loads it, a model the incoming route never
|
||||
/// chose while the outgoing route comes back on its catalog default.
|
||||
///
|
||||
/// When the outgoing route has no leaf and was resolving the alias, relocate
|
||||
/// that value onto the outgoing route's own canonical leaf, because it is real
|
||||
/// saved state, and only then clear the alias. Any other alias the incoming
|
||||
/// route already shadows with its own leaf is inert and stays: deleting a
|
||||
/// saved choice to satisfy validation of a value nothing resolves is data
|
||||
/// loss, not a repair. Likewise an unnamed custom route stores its
|
||||
/// model in the alias itself and has no leaf to receive it. If the incoming
|
||||
/// route cannot shadow that value, refuse the switch atomically and ask for an
|
||||
/// explicit destination model instead of saving an unloadable configuration.
|
||||
///
|
||||
/// `previous` is the document's configuration before the switch; `incoming` is
|
||||
/// the identity now selected. Every route writer calls this, so no writer can
|
||||
/// keep a private rule about which route owns the root alias.
|
||||
pub(crate) fn reconcile_root_model_aliases(
|
||||
doc: &mut toml_edit::DocumentMut,
|
||||
previous: &codewhale_runtime::config::Config,
|
||||
incoming: &codewhale_runtime::config::ProviderIdentity,
|
||||
) -> anyhow::Result<()> {
|
||||
// A freshly admitted id-less root keeps its legacy alias as a receipt.
|
||||
// Its effective model is written to the canonical leaf created by this
|
||||
// same-lock migration; the alias is never another route's saved choice.
|
||||
if incoming.provider == ProviderKind::Custom && incoming.persisted_id().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
// The outgoing route was resolving the alias as its own model. Move it
|
||||
// onto that route's leaf even when the incoming route could load it: a
|
||||
// pass-through incoming route would otherwise inherit a model it never
|
||||
// chose, and the outgoing route would come back on its catalog default.
|
||||
if let Some((outgoing, value)) = previous.root_model_alias_owned_by_outgoing(incoming) {
|
||||
persistence::set_verified_provider_model_document(doc, previous, &outgoing, &value)?;
|
||||
unset_root_model_aliases(doc)?;
|
||||
return Ok(());
|
||||
}
|
||||
// Below, only `default_text_model` is read. The legacy root `model` key is
|
||||
// never what blocks a load: `Config::default_model` already refuses to
|
||||
// route a foreign legacy value to a provider that cannot serve it, and
|
||||
// `Config::validate` does not consult it, so relocating it would move a
|
||||
// value nothing is asking about. It is still cleared together with the
|
||||
// alias that shadows it (`unset_root_model_aliases`).
|
||||
const ROOT_KEY: &str = "default_text_model";
|
||||
let Some(value) = doc
|
||||
.get(ROOT_KEY)
|
||||
.and_then(toml_edit::Item::as_str)
|
||||
.map(str::to_owned)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut switched = codewhale_runtime::config::parse_config_base(&doc.to_string())
|
||||
.map_err(|_| anyhow::anyhow!("Could not parse switched route; contents omitted"))?;
|
||||
crate::plugins::providers::apply_startup_providers(&mut switched)?;
|
||||
// `Config::validate` is the single authority on what the incoming route can
|
||||
// serve, so a writer cannot disagree with the loader. Act only when this
|
||||
// alias is what the loader rejects: a document already broken for an
|
||||
// unrelated reason is not this writer's to rewrite.
|
||||
let mut without_alias = switched.clone();
|
||||
without_alias.default_text_model = None;
|
||||
if switched
|
||||
.provider_config_for(incoming)
|
||||
.and_then(|entry| entry.model.as_deref())
|
||||
.is_some()
|
||||
|| switched.validate().is_ok()
|
||||
|| without_alias.validate().is_err()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
// An empty or control-bearing alias names no saved model. Nothing to
|
||||
// relocate, and clearing it loses nothing.
|
||||
if value.trim().is_empty() || value.chars().any(char::is_control) {
|
||||
unset_root_model_aliases(doc)?;
|
||||
return Ok(());
|
||||
}
|
||||
// No leaf can hold this value. Keep the only copy of the user's choice
|
||||
// rather than discard it for a document the incoming route loads as soon as
|
||||
// it saves a model of its own.
|
||||
let outgoing = previous.active_provider_identity().ok();
|
||||
if outgoing.as_ref().is_some_and(|outgoing| {
|
||||
outgoing != incoming
|
||||
&& outgoing.provider == ProviderKind::Custom
|
||||
&& outgoing.persisted_id().is_none()
|
||||
}) {
|
||||
// This same-lock canonicalizer copied the root model before moving
|
||||
// its endpoint. Clear an alias only when that exact outgoing leaf
|
||||
// proves the choice survives; otherwise retain the refusal.
|
||||
if outgoing.as_ref().is_some_and(|outgoing| {
|
||||
previous
|
||||
.provider_config_for(outgoing)
|
||||
.and_then(|entry| entry.model.as_deref())
|
||||
== Some(value.as_str())
|
||||
}) {
|
||||
return unset_root_model_aliases(doc);
|
||||
}
|
||||
anyhow::bail!(
|
||||
"Choose a model for the destination provider before switching from a legacy custom connection; the saved configuration was not changed"
|
||||
);
|
||||
}
|
||||
let Some(outgoing) = outgoing.as_ref().filter(|outgoing| *outgoing != incoming) else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut scoped = switched;
|
||||
scoped
|
||||
.scope_to_provider_identity(outgoing)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
if scoped
|
||||
.provider_config_for(outgoing)
|
||||
.and_then(|entry| entry.model.as_deref())
|
||||
.is_none()
|
||||
{
|
||||
persistence::set_verified_provider_model_document(doc, previous, outgoing, &value)?;
|
||||
}
|
||||
// The outgoing route either already saved its own choice or has just
|
||||
// received this one, so the alias is now a shadowed duplicate that only
|
||||
// blocks the incoming route.
|
||||
unset_root_model_aliases(doc)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the root model alias: `default_text_model` and the legacy root
|
||||
/// `model` that `Config::default_model` falls back to when the former is
|
||||
/// unset. While `default_text_model` is present the legacy key resolves on no
|
||||
/// route, so removing only the alias would resurrect a stale legacy model on
|
||||
/// the incoming route (a pass-through route would then use a model nobody
|
||||
/// chose for it). When the legacy key *is* the alias, the caller has already
|
||||
/// moved its value onto the outgoing route's leaf.
|
||||
fn unset_root_model_aliases(doc: &mut toml_edit::DocumentMut) -> anyhow::Result<()> {
|
||||
persistence::unset_document_value(doc, &["default_text_model"])?;
|
||||
persistence::unset_document_value(doc, &["model"])?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -17152,7 +17152,10 @@ impl RuntimeThreadManager {
|
||||
submission_id: None,
|
||||
};
|
||||
let op = match &input_source {
|
||||
RuntimeTurnInputSource::Review(request) => Op::Review { turn: spec, request: request.clone() },
|
||||
RuntimeTurnInputSource::Review(request) => Op::Review {
|
||||
turn: Box::new(spec),
|
||||
request: request.clone(),
|
||||
},
|
||||
_ => Op::SendMessage(spec),
|
||||
};
|
||||
|
||||
|
||||
@@ -20231,7 +20231,7 @@ fn restart_rebuild_restores_tool_call_identity_from_persisted_items() -> Result<
|
||||
let turn = manager.store.load_turn(&checkpoint_turn.id)?;
|
||||
let items = manager.prepared_items_for_turns(std::slice::from_ref(&turn))?;
|
||||
assert_eq!(
|
||||
manager.history_from_records(&[turn.clone()], &items)?,
|
||||
manager.history_from_records(std::slice::from_ref(&turn), &items)?,
|
||||
expected
|
||||
);
|
||||
first.metadata.as_mut().unwrap()["tool_input"] = json!(r#"{"path":"changed"}"#);
|
||||
|
||||
@@ -245,17 +245,16 @@ impl UserInputRequest {
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(UserInputIntent::PlanReview { approve, .. }) = &question.intent {
|
||||
if !labels.contains(approve)
|
||||
if let Some(UserInputIntent::PlanReview { approve, .. }) = &question.intent
|
||||
&& (!labels.contains(approve)
|
||||
|| question
|
||||
.detail
|
||||
.as_ref()
|
||||
.is_none_or(|detail| detail.trim().is_empty())
|
||||
{
|
||||
return Err(ToolError::invalid_input(
|
||||
"Plan review requires detail and an offered approve choice",
|
||||
));
|
||||
}
|
||||
.is_none_or(|detail| detail.trim().is_empty()))
|
||||
{
|
||||
return Err(ToolError::invalid_input(
|
||||
"Plan review requires detail and an offered approve choice",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -16,6 +16,7 @@ mod response_body;
|
||||
pub use response_body::read_response_body_capped;
|
||||
|
||||
/// A writer that counts bytes written without storing them.
|
||||
#[derive(Default)]
|
||||
pub struct CountingWriter {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
@@ -3447,6 +3447,12 @@ restart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the
|
||||
host's sandbox does on each platform are in
|
||||
[Writing an extension tool](EXTENSIONS.md).
|
||||
|
||||
`[extension_host] mcp_backend` (user config only) chooses the MCP client
|
||||
independently of the `extension_host` flag: `"host"` (default) uses the official
|
||||
MCP SDK behind the Rust broker; `"rust"` keeps the previous Rust client for one
|
||||
release. A host refusal never falls back to Rust. See
|
||||
[MCP connection lifecycle](MCP.md#connection-lifecycle).
|
||||
|
||||
Every flag has a row in [`docs/features.toml`](features.toml), the feature
|
||||
registry; a test fails when a flag and its row disagree.
|
||||
|
||||
|
||||
@@ -40,6 +40,19 @@ changelog or release notes, and this contributor record.
|
||||
## Contributors by time
|
||||
|
||||
<details open>
|
||||
<summary><strong>Unreleased (0.10.3 candidate)</strong></summary>
|
||||
|
||||
**Contributions**
|
||||
|
||||
- **[Boyce / Aboysky](https://github.com/Aboysky)** — refreshed the README translation source stamps ([#7104](https://github.com/codewhale-hq/Codewhale/pull/7104)).
|
||||
- **[7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — per-scope instruction budgets with visible truncation ([#7086](https://github.com/codewhale-hq/Codewhale/pull/7086)).
|
||||
- **[Lstarsky0](https://github.com/Lstarsky0)** — `/diff` pager resize, goal-clear wording and unpriced `/status` cost ([#7087](https://github.com/codewhale-hq/Codewhale/pull/7087), [#7089](https://github.com/codewhale-hq/Codewhale/pull/7089), [#7090](https://github.com/codewhale-hq/Codewhale/pull/7090)).
|
||||
- **[gaord](https://github.com/gaord)** — a running turn's cost and context ([#7074](https://github.com/codewhale-hq/Codewhale/pull/7074)).
|
||||
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — relocated state root before session-side state ([#7097](https://github.com/codewhale-hq/Codewhale/pull/7097)).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>v0.10.2 — terminal, recovery and reliability</strong></summary>
|
||||
|
||||
**Contributions**
|
||||
|
||||
+3
-3
@@ -17,8 +17,8 @@ Global key chords are not yet user-configurable — tracked for a future release
|
||||
| `Ctrl-C` | Cancel current turn / dismiss modal / clear a non-empty draft (recoverable — see `Ctrl-Z`) / arm-then-confirm quit |
|
||||
| `Ctrl-B` | Release the current shell wait so the turn can continue; foreground commands move into `/jobs`, and background tasks keep running. Use `/jobs` or `Bash` with `action: "wait"` to inspect output |
|
||||
| `Ctrl-D` | Quit (only when the composer is empty) |
|
||||
| `Tab` | When the composer is empty, cycle TUI mode: Plan → Work → Operate → Plan |
|
||||
| `Shift+Tab` | Cycle permission posture: Ask → Auto-Review → Full Access. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open) |
|
||||
| `Tab` | When the composer is empty, cycle TUI mode: Plan → Work → Operate → Plan. Works while a turn is running: the change applies at the agent's next step in the same turn, and the status line says so until it does |
|
||||
| `Shift+Tab` | Cycle permission posture: Ask → Auto-Review → Full Access. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open). During a turn the change applies at the agent's next step; a call you already approved runs only if the new posture is equal or broader. Model, thinking level and provider stay locked until the turn ends |
|
||||
| `Ctrl-T` | Cycle reasoning effort for the active model. Walks the same ladder as `/model` and `/effort` (catalog or documented route dialect). Always-thinking models omit `off`; Grok 4.6 includes `xhigh`. |
|
||||
| `Ctrl-Shift-T` | Toggle live transcript overlay (sticky-tail auto-scroll) |
|
||||
| `Ctrl-R` | Open the resume-session picker |
|
||||
@@ -46,7 +46,7 @@ Editing the message you're about to send.
|
||||
|-----------------------------|---------------------------------------------------------|
|
||||
| `Enter` | Send when idle; queue while busy; with an empty composer, send the next queued follow-up now |
|
||||
| `Shift-Enter` / `Alt-Enter` / `Ctrl-J` | Insert a newline without sending (idle or busy) |
|
||||
| `Ctrl-Enter` / `Cmd-Enter` | Send into the current turn; send normally when idle (when the terminal reports it distinctly) |
|
||||
| `Ctrl-Enter` / `Cmd-Enter` | Steer: send into the current turn, closing the in-flight model request at once (reasoning, text or tool call) and continuing the same turn with your message; send normally when idle (when the terminal reports it distinctly). Text already shown is kept; unfinished reasoning and partial tool calls are discarded, never run. A second `Enter` within 500 ms of queueing a message while busy steers too |
|
||||
| `Ctrl-U` | Clear the whole draft (recoverable — see `Ctrl-Z`) |
|
||||
| `Ctrl-Z` | Restore the cleared draft (only while the composer is empty) |
|
||||
| `Ctrl-C` | Clear the whole draft when no turn is running (recoverable — see `Ctrl-Z`) |
|
||||
|
||||
+12
@@ -594,6 +594,18 @@ codewhale mcp tools codewhale
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
As a client, Codewhale connects servers through the official MCP SDK (2.4)
|
||||
behind its Rust broker by default (`[extension_host] mcp_backend = "host"` in
|
||||
your user config; `"rust"` selects the previous Rust client and stays available
|
||||
for one release, and a host refusal never falls back to it). Servers that speak
|
||||
the 2026-07-28 revision are discovered with `server/discover`. Other stdio and
|
||||
HTTP servers, including ones that answer the discovery probe with an error or
|
||||
an unrelated result, connect through the legacy `initialize` handshake, and
|
||||
configured SSE servers keep theirs. A server that rejects sign-in reports its
|
||||
real 401 with the `/mcp login` hint, and one whose command cannot start says
|
||||
why. Rust keeps process, network, credential, catalog, approval and
|
||||
cancellation authority.
|
||||
|
||||
Session boot connects in the background: every enabled, allowed server —
|
||||
user-configured or contributed by an enabled plugin — is started at session
|
||||
start, concurrently and without holding the first turn. A plugin server passes
|
||||
|
||||
@@ -219,9 +219,14 @@ def report_tui_catalog_contract(tui_config_rs: str) -> list[str]:
|
||||
errors.append(f"retired provider enum consumer: {path.relative_to(ROOT)}")
|
||||
if re.search(r"KIND_LOOKUP|FROM_KIND_LOOKUP|ProviderKind::from_kind\(", source):
|
||||
errors.append(f"retired provider ordinal bridge: {path.relative_to(ROOT)}")
|
||||
for signature in ("pub(crate) fn active_provider_identity", "pub(crate) fn provider_identities", "pub(crate) fn resolve_provider_selection_identity", "pub(crate) fn verify_provider_identity"):
|
||||
if signature not in tui_config_rs:
|
||||
errors.append(f"missing canonical config identity boundary: {signature}")
|
||||
for function_name in (
|
||||
"active_provider_identity",
|
||||
"provider_identities",
|
||||
"resolve_provider_selection_identity",
|
||||
"verify_provider_identity",
|
||||
):
|
||||
if not re.search(rf"\bpub(?:\(crate\))?\s+fn\s+{function_name}\b", tui_config_rs):
|
||||
errors.append(f"missing canonical config identity boundary: {function_name}")
|
||||
descriptor_source = read(ROOT / "crates/config/src/descriptors.rs")
|
||||
if "PROVIDER_COMPATIBILITY" not in descriptor_source or "pub fn compatibility_for_selector" not in descriptor_source:
|
||||
errors.append("released presentation identities must use the existing generated descriptor owner")
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export the Codewhale palettes to the other Codewhale clients.
|
||||
|
||||
The versioned `vendor/codewhale-design/tokens.json` snapshot owns the GPUI
|
||||
colors, radii, typography fallbacks and focus geometry. The adapter preserves
|
||||
this site's existing light/OS-dark/pinned-dark roles. The portable artifact's
|
||||
The versioned `vendor/codewhale-design/tokens.json` snapshot owns the
|
||||
website's colors, type roles, spacing, radii, motion, easing, elevation,
|
||||
breakpoints and focus geometry; `web/app/styles/tokens-roles.css` names the
|
||||
site's roles over them (web/docs/BRAND_COLOR_MAPPING.md). The portable artifact's
|
||||
own generator checks its digest before exporting. Update the entire vendored
|
||||
folder from the app's design package; never edit its JSON here.
|
||||
|
||||
@@ -101,9 +102,10 @@ def gpui_tokens(data):
|
||||
aliases = {
|
||||
"BG": "background", "TEXT": "foreground", "PANEL": "surface",
|
||||
"TEXT_MUTED": "muted_foreground", "BORDER": "border", "SIDEBAR": "sidebar",
|
||||
"PRIMARY": "primary", "ON_PRIMARY": "primary_foreground", "ACCENT": "hover",
|
||||
"LIST_ACTIVE": "selected", "ATTENTION": "attention", "LIVE": "live",
|
||||
"DANGER": "danger", "BORDER_STRONG": "border_strong",
|
||||
"PRIMARY": "primary", "ON_PRIMARY": "primary_foreground", "ACCENT": "accent",
|
||||
"HOVER": "hover", "LIST_ACTIVE": "selected", "ATTENTION": "attention", "LIVE": "live",
|
||||
"DANGER": "danger", "BORDER_STRONG": "border_strong", "OCEAN_DEPTH": "ocean_depth",
|
||||
"DIFF_ADD": "diff_add", "DIFF_REMOVE": "diff_remove",
|
||||
}
|
||||
return [
|
||||
(f"{prefix}_{name}", tuple(bytes.fromhex(palette[key])))
|
||||
@@ -120,6 +122,25 @@ def gpui_geometry(data, digest):
|
||||
lines.append(f" --gpui-{section}-{name}: {value}px;")
|
||||
for name in ["selection_opacity", "primary_hover_opacity"]:
|
||||
lines.append(f" --gpui-{name.replace('_', '-')}: {data[name]};")
|
||||
for role, spec in data["type_roles"].items():
|
||||
lines += [f" --gpui-type-{role}-size: {spec['size_px'] / 16:g}rem;",
|
||||
f" --gpui-type-{role}-line: {spec['line_px'] / spec['size_px']:.4g};",
|
||||
f" --gpui-type-{role}-weight: {spec['weight']};"]
|
||||
for name, value in data["motion"].items():
|
||||
if name.startswith("duration_") or name.startswith("loop_") and name.endswith("_ms"):
|
||||
lines.append(f" --gpui-motion-{name.removesuffix('_ms').replace('_', '-')}: {value}ms;")
|
||||
lines.append(f" --gpui-motion-arrive-rise: {data['motion']['arrive_rise_px']}px;")
|
||||
for name, curve in data["easing"].items():
|
||||
lines.append(f" --gpui-easing-{name}: cubic-bezier({', '.join(f'{v:g}' for v in curve)});")
|
||||
e = data["elevation"]
|
||||
for kind in ["popover", "dialog"]:
|
||||
for mode in ["light", "dark"]:
|
||||
lines.append(f" --gpui-elevation-{kind}-{mode}: 0 {e[kind + '_y_px']}px "
|
||||
f"{e[kind + '_blur_px']}px rgb(0 0 0 / {e[kind + '_alpha_' + mode]:g});")
|
||||
for name, value in data["breakpoints"].items():
|
||||
lines.append(f" --gpui-breakpoint-{name.removesuffix('_px')}: {value}px;")
|
||||
rise, deep = data["ombre"]["rise"], data["ombre"]["deep"]
|
||||
lines.append(f" --gpui-ombre: linear-gradient(135deg, #{rise}, #{deep});")
|
||||
fallbacks = ", ".join(json.dumps(v) for v in data["typography"]["fallbacks"])
|
||||
lines += [f" --gpui-font-fallbacks: {fallbacks};",
|
||||
f" --gpui-mono-size: {data['typography']['mono_px'] / 16:g}rem;", "}"]
|
||||
|
||||
Vendored
+44
-5
@@ -2,6 +2,9 @@
|
||||
|
||||
Keep every Codewhale surface on one set of design tokens.
|
||||
|
||||
[SYSTEM.md](SYSTEM.md) is the design system: the product model, how tokens are used, components per
|
||||
medium, patterns and the current audit. [DIRECTION.md](DIRECTION.md) is how it should feel.
|
||||
|
||||
This repository is the source of truth for Codewhale's colours, type, spacing,
|
||||
radii and motion constants. The desktop app, the website, the terminal UI
|
||||
(`codewhale-ratatui`) and the mobile app each vendor a copy and verify it at
|
||||
@@ -18,20 +21,56 @@ web-next vendors this folder and imports `tokens.css`. No network access or
|
||||
sibling checkout is required to build a consumer.
|
||||
|
||||
Change the JSON here, increment its version, run `python3 generate.py`,
|
||||
and copy this entire folder into each consumer's `vendor/codewhale-design`.
|
||||
and use `scripts/sync-to.sh` to copy the runtime token files into each consumer's
|
||||
`vendor/codewhale-design`. Design studies and visual proofs stay in this repository.
|
||||
Run the generator with `--check` in each repository. Generated files include the
|
||||
source digest; consumer tests reject drift. Layout remains native to each
|
||||
surface. Focus and reduced-motion settings must remain accessible on each host.
|
||||
|
||||
Typography and palette come from the shipping desktop theme. Icons retain the
|
||||
Typography and palette come from the shipping desktop theme. `primary` fills
|
||||
actions and switches in neutral ink; `accent` is the one quiet blue, for focus
|
||||
rings, links, text selection and the caret; the logo ombre stays on the whale
|
||||
and the sign-in page. Icons retain the
|
||||
24-unit, 1.7-pixel rounded stroke family; semantic names and accessible labels
|
||||
stay with the host controls. Desktop spring constants and reduced-motion poll
|
||||
cadence are shared without introducing decorative animation on web or phones.
|
||||
stay with the host controls. Motion is shared too: durations (state, arrive,
|
||||
exit, panel, linger), the arrive rise, the two loop periods (`beat` for
|
||||
something that needs you, `breathe` for work in progress), the loop frame cap,
|
||||
three easing curves (`out`, `in`, `panel`), state glows, the logo ombre and the
|
||||
popover elevation reach Rust as constants and CSS as `--motion-*`, `--glow-*`,
|
||||
`--ombre-*` and `--elevation-popover`. Desktop spring constants and the
|
||||
reduced-motion poll cadence stay Rust-only. Under reduced motion every host
|
||||
runs durations at zero, stops loops and keeps glows static.
|
||||
|
||||
`stream` paces how received reply text is uncovered, the same way on every
|
||||
client: it follows the stream's arrival rate averaged over `rate_window_ms`,
|
||||
uncovers the first receipt within `reveal_first_ms`, never lets hidden text
|
||||
trail the received text by more than `reveal_lag_ms`, shows a receipt of at
|
||||
most `instant_graphemes` at once when nothing is waiting, and shows a backlog
|
||||
over `max_pending_bytes` (UTF-16 units on the web) at once. Rust reads them as
|
||||
`STREAM_*`; the web reads `tokens.json`. Completion, corrections and reduced
|
||||
motion always show the exact received text immediately.
|
||||
|
||||
`icons/` holds the sea glyphs DIRECTION.md names, on the same 24-unit grid at a
|
||||
1.7-pixel round stroke, each with its word as `aria-label`: sonar (searching,
|
||||
thinking), tide (real progress, waking), shell (receipt) and kelp (divider).
|
||||
A consumer copies the file it uses beside its own icons; a glyph never stands
|
||||
without its word on screen.
|
||||
|
||||
CSS consumers use `font-family: var(--font-family), var(--font-fallbacks), sans-serif`
|
||||
to keep the shared family and CJK fallbacks together. The generator rejects
|
||||
selection and primary-hover opacity values outside the inclusive 0–1 range,
|
||||
and any palette that misses WCAG AA contrast in either mode: every text role
|
||||
at 4.5:1 (`muted_foreground` at 5.5:1) and `border_strong` at 3:1 on
|
||||
`background`, `surface`, `sidebar`, `hover` and `selected`, and `primary_foreground` on `primary` at 4.5:1. A new
|
||||
`background`, `surface`, `sidebar`, `hover`, `selected`, `ocean_depth` and the diff rows `diff_add` and `diff_remove`, `primary_foreground` on `primary` and on `accent` at 4.5:1, and dark
|
||||
`foreground` on the ombre's deep stop at 4.5:1 (its rising stop is never a text ground). It also rejects
|
||||
glow and elevation alphas outside 0–1 and any easing curve whose control points leave [0, 1], so nothing
|
||||
overshoots. A new
|
||||
colour, such as a named blue, enters the tokens only once it passes.
|
||||
|
||||
## Design studies and implementation handoffs
|
||||
|
||||
[Claude desktop → Codewhale, October 9, 2026](studies/claude-parity-20261009/README.md)
|
||||
contains the editable Figma screen map, reusable component inventory, visual
|
||||
proofs and frame-specific implementation packets. Its observed reference
|
||||
behavior and proposed states are recorded separately. The study uses the
|
||||
existing tokens; it does not introduce another palette or change shipped defaults.
|
||||
Vendored
+68
-13
@@ -11,14 +11,19 @@ ROOT = Path(__file__).resolve().parent
|
||||
|
||||
# WCAG 2.x contrast every palette must hold, in both modes, before anything is
|
||||
# generated: text roles at 4.5:1 and control edges at 3:1 on each ground a
|
||||
# role can sit on, including a hovered or selected row.
|
||||
GROUNDS = ["background", "surface", "sidebar", "hover", "selected"]
|
||||
TEXT = ["foreground", "muted_foreground", "primary", "live", "attention", "danger"]
|
||||
# role can sit on, including a hovered or selected row and the deepest point
|
||||
# of the ocean field behind the working area, and the added and removed rows
|
||||
# of a diff, which carry code and their +/− marks.
|
||||
GROUNDS = ["background", "surface", "sidebar", "hover", "selected", "ocean_depth", "diff_add", "diff_remove"]
|
||||
TEXT = ["foreground", "muted_foreground", "primary", "accent", "live", "attention", "danger"]
|
||||
# Secondary text carries labels, metadata and status words people must read at
|
||||
# small sizes, so it holds 5.5:1 on every ground, above the AA floor.
|
||||
TEXT_FLOORS = {"muted_foreground": 5.5}
|
||||
EDGES = ["border_strong"]
|
||||
FILLED = [("primary_foreground", "primary")]
|
||||
FILLED = [("primary_foreground", "primary"), ("primary_foreground", "accent")]
|
||||
# The ombre's deep stop can carry body text on the dark ground; its rising
|
||||
# stop is a brand colour only and never a text ground.
|
||||
OMBRE_TEXT = [("foreground", "deep", 4.5)]
|
||||
|
||||
def luminance(hex6):
|
||||
channels = [int(hex6[i:i + 2], 16) / 255 for i in (0, 2, 4)]
|
||||
@@ -29,7 +34,7 @@ def contrast(a, b):
|
||||
hi, lo = sorted([luminance(a), luminance(b)], reverse=True)
|
||||
return (hi + 0.05) / (lo + 0.05)
|
||||
|
||||
def contrast_violations(colors):
|
||||
def contrast_violations(colors, ombre):
|
||||
pairs = [(fg, bg, TEXT_FLOORS.get(fg, 4.5)) for fg in TEXT for bg in GROUNDS]
|
||||
pairs += [(edge, bg, 3.0) for edge in EDGES for bg in GROUNDS]
|
||||
pairs += [(fg, bg, 4.5) for fg, bg in FILLED]
|
||||
@@ -39,22 +44,35 @@ def contrast_violations(colors):
|
||||
ratio = contrast(palette[fg], palette[bg])
|
||||
if ratio < floor:
|
||||
found.append(f"{mode} {fg} on {bg} is {ratio:.2f}:1, needs {floor}:1")
|
||||
for fg, stop, floor in OMBRE_TEXT:
|
||||
ratio = contrast(colors["dark"][fg], ombre[stop])
|
||||
if ratio < floor:
|
||||
found.append(f"dark {fg} on ombre {stop} is {ratio:.2f}:1, needs {floor}:1")
|
||||
return found
|
||||
|
||||
def render():
|
||||
source = (ROOT / "tokens.json").read_bytes()
|
||||
data = json.loads(source)
|
||||
for name in ["selection_opacity", "primary_hover_opacity"]:
|
||||
value = data[name]
|
||||
unit = {name: data[name] for name in ["selection_opacity", "primary_hover_opacity"]}
|
||||
unit |= {f"glow.{name}": v for name, v in data["glow"].items() if name.endswith("_alpha")}
|
||||
unit |= {f"elevation.{name}": v for name, v in data["elevation"].items() if "_alpha" in name}
|
||||
for name, value in unit.items():
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not 0 <= value <= 1:
|
||||
raise ValueError(f"{name} must be a number between 0 and 1")
|
||||
# No overshoot or undershoot: a curve's x and y control points stay in [0, 1].
|
||||
for name, curve in data["easing"].items():
|
||||
if len(curve) != 4 or not all(isinstance(v, (int, float)) and 0 <= v <= 1 for v in curve):
|
||||
raise ValueError(f"easing.{name} must be four control points between 0 and 1")
|
||||
stream = data["stream"]
|
||||
if not all(isinstance(v, int) and v > 0 for v in stream.values()) or stream["reveal_first_ms"] > stream["reveal_lag_ms"]:
|
||||
raise ValueError("stream values must be positive integers, with reveal_first_ms no longer than reveal_lag_ms")
|
||||
digest = hashlib.sha256(source).hexdigest()
|
||||
header = f"Generated from Codewhale GPUI design {data['version']}; sha256 {digest}. Do not edit."
|
||||
keys = list(data["colors"]["dark"])
|
||||
assert set(keys) == set(data["colors"]["light"])
|
||||
for palette in data["colors"].values():
|
||||
for palette in [*data["colors"].values(), data["ombre"]]:
|
||||
assert all(len(v) == 6 and all(c in "0123456789abcdef" for c in v) for v in palette.values())
|
||||
violations = contrast_violations(data["colors"])
|
||||
violations = contrast_violations(data["colors"], data["ombre"])
|
||||
if violations:
|
||||
raise ValueError("Contrast below WCAG AA:\n " + "\n ".join(violations))
|
||||
rust = [f"// {header}", "#![allow(dead_code)]", "", f'pub const VERSION: &str = "{data["version"]}";',
|
||||
@@ -70,29 +88,61 @@ def render():
|
||||
"pub const FONT_FALLBACKS: &[&str] = &["]
|
||||
rust += [f" {json.dumps(v)}," for v in data["typography"]["fallbacks"]]
|
||||
rust += ["];"]
|
||||
for section in ["spacing", "radius", "focus", "icons", "motion"]:
|
||||
for section in ["spacing", "radius", "focus", "icons", "motion", "stream", "glow", "elevation"]:
|
||||
for name, value in data[section].items():
|
||||
rust += [f"pub const {section.upper()}_{name.upper()}: f32 = {float(value)};"]
|
||||
for name, curve in data["easing"].items():
|
||||
rust += [f"pub const EASING_{name.upper()}: [f32; 4] = [{', '.join(str(float(v)) for v in curve)}];"]
|
||||
for name, value in data["ombre"].items():
|
||||
rust += [f"pub const OMBRE_{name.upper()}: u32 = 0x{value};"]
|
||||
for name in ["selection_opacity", "primary_hover_opacity"]:
|
||||
rust += [f"pub const {name.upper()}: f32 = {float(data[name])};"]
|
||||
rust += [f'pub const MONO_PX: f32 = {float(data["typography"]["mono_px"])};']
|
||||
roles = data["type_roles"]
|
||||
for name, role in roles.items():
|
||||
if not (isinstance(role["size_px"], (int, float)) and role["line_px"] >= role["size_px"] and role["weight"] in range(100, 1000)):
|
||||
raise ValueError(f"type_roles.{name} needs size_px, a line_px no smaller than it, and a weight from 100 to 900")
|
||||
for key in ["size_px", "line_px", "weight"]:
|
||||
rust += [f"pub const TYPE_ROLE_{name.upper()}_{key.upper()}: f32 = {float(role[key])};"]
|
||||
for section in ["size", "breakpoints"]:
|
||||
for name, value in data[section].items():
|
||||
rust += [f"pub const {section.upper()}_{name.upper()}: f32 = {float(value)};"]
|
||||
if data["size"]["touch_target_px"] < 44:
|
||||
raise ValueError("size.touch_target_px must be at least 44")
|
||||
for name in ["body_px", "caption_px", "prose_px", "heading_px", "title_px"]:
|
||||
if name in data["typography"]:
|
||||
rust += [f'pub const TYPE_{name.upper()}: f32 = {float(data["typography"][name])};']
|
||||
# Existing web names remain aliases, so every current component migrates together.
|
||||
aliases = {"outside":"sidebar","surface":"background","panel":"surface","text":"foreground",
|
||||
"muted":"muted_foreground","line":"border","accent":"primary","button":"primary",
|
||||
"muted":"muted_foreground","line":"border","accent":"accent","button":"primary",
|
||||
"button-text":"primary_foreground","attention":"attention","hover":"hover","selected":"selected",
|
||||
"live":"live","danger":"danger","border-strong":"border_strong"}
|
||||
"live":"live","danger":"danger","border-strong":"border_strong","ocean-depth":"ocean_depth",
|
||||
"diff-add":"diff_add","diff-remove":"diff_remove"}
|
||||
css = [f"/* {header} */"]
|
||||
for mode, palette in data["colors"].items():
|
||||
css += [":root {" if mode == "dark" else '[data-theme="light"] {']
|
||||
css += [f" --{alias}: #{palette[key]};" for alias, key in aliases.items()]
|
||||
css += [f' --selection: #{palette["primary"]}{round(data["selection_opacity"] * 255):02x};',
|
||||
elevation = data["elevation"]
|
||||
css += [f' --selection: #{palette["accent"]}{round(data["selection_opacity"] * 255):02x};',
|
||||
f' --elevation-popover: 0 {elevation["popover_y_px"]}px {elevation["popover_blur_px"]}px rgb(0 0 0 / {elevation[f"popover_alpha_{mode}"]});',
|
||||
f' --elevation-dialog: 0 {elevation["dialog_y_px"]}px {elevation["dialog_blur_px"]}px rgb(0 0 0 / {elevation[f"dialog_alpha_{mode}"]});',
|
||||
f" color-scheme: {mode};", "}"]
|
||||
css += [":root {"]
|
||||
for section in ["spacing", "radius", "focus"]:
|
||||
css += [f" --{section}-{name}: {value}px;" for name, value in data[section].items()]
|
||||
motion = data["motion"]
|
||||
css += [f" --motion-duration-{name[9:-3]}: {value}ms;" for name, value in motion.items()
|
||||
if name.startswith("duration_")]
|
||||
css += [f" --motion-{name[:-3].replace('_', '-')}: {value}ms;" for name, value in motion.items()
|
||||
if name.startswith("loop_") and name.endswith("_ms")]
|
||||
css += [f' --motion-arrive-rise: {motion["arrive_rise_px"]}px;']
|
||||
css += [f" --motion-ease-{name}: cubic-bezier({', '.join(str(v) for v in curve)});"
|
||||
for name, curve in data["easing"].items()]
|
||||
glow = data["glow"]
|
||||
css_alias = {"primary": "accent", "live": "live", "attention": "attention"}
|
||||
css += [f' --glow-{name[:-6]}: 0 0 {glow["blur_px"]}px color-mix(in srgb, var(--{css_alias[name[:-6]]}) {round(value * 100)}%, transparent);'
|
||||
for name, value in glow.items() if name.endswith("_alpha")]
|
||||
css += [f" --ombre-{name}: #{value};" for name, value in data["ombre"].items()]
|
||||
css += [f' --icon-stroke: {data["icons"]["stroke"]};',
|
||||
f' --font-family: {json.dumps(data["typography"]["family"], ensure_ascii=False)};',
|
||||
f' --font-fallbacks: {", ".join(json.dumps(v, ensure_ascii=False) for v in data["typography"]["fallbacks"])};',
|
||||
@@ -101,6 +151,11 @@ def render():
|
||||
for name in ["caption_px", "prose_px", "heading_px", "title_px"]:
|
||||
if name in data["typography"]:
|
||||
css += [f' --font-{name[:-3]}-size: {data["typography"][name]}px;']
|
||||
for name, role in data["type_roles"].items():
|
||||
css += [f' --type-{name}-size: {role["size_px"]}px;', f' --type-{name}-line: {role["line_px"]}px;',
|
||||
f' --type-{name}-weight: {role["weight"]};']
|
||||
css += [f' --size-{name[:-3].replace("_", "-")}: {value}px;' for name, value in data["size"].items()]
|
||||
css += [f' --breakpoint-{name[:-3]}: {value}px;' for name, value in data["breakpoints"].items()]
|
||||
css += ["}"]
|
||||
return {"tokens.rs":"\n".join(rust) + "\n", "tokens.css":"\n".join(css) + "\n"}
|
||||
|
||||
|
||||
Vendored
+71
-11
@@ -1,21 +1,26 @@
|
||||
/* Generated from Codewhale GPUI design 1.1.2; sha256 433b18a41c526da0b5a1bbaa461c37feac987c7688fbe7de660acfbbdafa9bd5. Do not edit. */
|
||||
/* Generated from Codewhale GPUI design 2.3.0; sha256 9e74fb8f8fbfebb6b90a9740b324c56405277886e61273e2a0cb682797a8a6f9. Do not edit. */
|
||||
:root {
|
||||
--outside: #191a1c;
|
||||
--surface: #202123;
|
||||
--panel: #2a2b2e;
|
||||
--text: #efeeeb;
|
||||
--muted: #c2c1bd;
|
||||
--muted: #b8b8b4;
|
||||
--line: #3b3c3f;
|
||||
--accent: #90b9ff;
|
||||
--button: #90b9ff;
|
||||
--button-text: #15243e;
|
||||
--accent: #a9c0de;
|
||||
--button: #e6e4df;
|
||||
--button-text: #202123;
|
||||
--attention: #e8b077;
|
||||
--hover: #303134;
|
||||
--selected: #37393d;
|
||||
--live: #9ec7b2;
|
||||
--danger: #e39a90;
|
||||
--border-strong: #828386;
|
||||
--selection: #90b9ff47;
|
||||
--ocean-depth: #202123;
|
||||
--diff-add: #22392f;
|
||||
--diff-remove: #45302f;
|
||||
--selection: #a9c0de47;
|
||||
--elevation-popover: 0 4px 8px rgb(0 0 0 / 0.4);
|
||||
--elevation-dialog: 0 12px 32px rgb(0 0 0 / 0.5);
|
||||
color-scheme: dark;
|
||||
}
|
||||
[data-theme="light"] {
|
||||
@@ -23,18 +28,23 @@
|
||||
--surface: #faf8f5;
|
||||
--panel: #ffffff;
|
||||
--text: #28292b;
|
||||
--muted: #535451;
|
||||
--muted: #4f504d;
|
||||
--line: #d9d5cf;
|
||||
--accent: #245bc7;
|
||||
--button: #245bc7;
|
||||
--button-text: #fbf5ee;
|
||||
--accent: #425c82;
|
||||
--button: #2e2f31;
|
||||
--button-text: #faf8f5;
|
||||
--attention: #86520d;
|
||||
--hover: #e8e5e0;
|
||||
--selected: #dfdcd6;
|
||||
--live: #3a6352;
|
||||
--danger: #9e3f36;
|
||||
--border-strong: #807c76;
|
||||
--selection: #245bc747;
|
||||
--ocean-depth: #faf8f5;
|
||||
--diff-add: #d2eadd;
|
||||
--diff-remove: #f8e1dd;
|
||||
--selection: #425c8247;
|
||||
--elevation-popover: 0 4px 8px rgb(0 0 0 / 0.16);
|
||||
--elevation-dialog: 0 12px 32px rgb(0 0 0 / 0.2);
|
||||
color-scheme: light;
|
||||
}
|
||||
:root {
|
||||
@@ -49,6 +59,22 @@
|
||||
--radius-sheet: 14px;
|
||||
--focus-width: 2px;
|
||||
--focus-offset: 3px;
|
||||
--motion-duration-state: 120ms;
|
||||
--motion-duration-arrive: 180ms;
|
||||
--motion-duration-exit: 120ms;
|
||||
--motion-duration-panel: 340ms;
|
||||
--motion-duration-linger: 1200ms;
|
||||
--motion-loop-beat: 1200ms;
|
||||
--motion-loop-breathe: 2400ms;
|
||||
--motion-arrive-rise: 4px;
|
||||
--motion-ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--motion-ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--motion-ease-panel: cubic-bezier(0.2, 0.1, 0.16, 1);
|
||||
--glow-primary: 0 0 6px color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
--glow-live: 0 0 6px color-mix(in srgb, var(--live) 35%, transparent);
|
||||
--glow-attention: 0 0 6px color-mix(in srgb, var(--attention) 40%, transparent);
|
||||
--ombre-rise: #1e8fd8;
|
||||
--ombre-deep: #0b48bb;
|
||||
--icon-stroke: 1.7;
|
||||
--font-family: "Shannon Sans";
|
||||
--font-fallbacks: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC";
|
||||
@@ -58,4 +84,38 @@
|
||||
--font-prose-size: 17px;
|
||||
--font-heading-size: 17px;
|
||||
--font-title-size: 22px;
|
||||
--type-caption-size: 12px;
|
||||
--type-caption-line: 16px;
|
||||
--type-caption-weight: 400;
|
||||
--type-label-size: 13px;
|
||||
--type-label-line: 18px;
|
||||
--type-label-weight: 400;
|
||||
--type-body-size: 14px;
|
||||
--type-body-line: 22px;
|
||||
--type-body-weight: 400;
|
||||
--type-prose-size: 16px;
|
||||
--type-prose-line: 26px;
|
||||
--type-prose-weight: 400;
|
||||
--type-heading-size: 17px;
|
||||
--type-heading-line: 24px;
|
||||
--type-heading-weight: 550;
|
||||
--type-title-size: 22px;
|
||||
--type-title-line: 28px;
|
||||
--type-title-weight: 550;
|
||||
--type-display-size: 28px;
|
||||
--type-display-line: 34px;
|
||||
--type-display-weight: 550;
|
||||
--type-code-size: 13px;
|
||||
--type-code-line: 20px;
|
||||
--type-code-weight: 400;
|
||||
--size-control: 28px;
|
||||
--size-row: 30px;
|
||||
--size-touch-target: 44px;
|
||||
--size-icon: 16px;
|
||||
--size-icon-large: 20px;
|
||||
--size-sidebar: 248px;
|
||||
--size-panel: 360px;
|
||||
--breakpoint-compact: 720px;
|
||||
--breakpoint-medium: 1024px;
|
||||
--breakpoint-wide: 1440px;
|
||||
}
|
||||
Vendored
+128
-11
@@ -1,37 +1,45 @@
|
||||
{
|
||||
"version": "1.1.2",
|
||||
"version": "2.3.0",
|
||||
"colors": {
|
||||
"dark": {
|
||||
"background": "202123",
|
||||
"foreground": "efeeeb",
|
||||
"surface": "2a2b2e",
|
||||
"muted_foreground": "c2c1bd",
|
||||
"muted_foreground": "b8b8b4",
|
||||
"border": "3b3c3f",
|
||||
"sidebar": "191a1c",
|
||||
"primary": "90b9ff",
|
||||
"primary_foreground": "15243e",
|
||||
"primary": "e6e4df",
|
||||
"primary_foreground": "202123",
|
||||
"accent": "a9c0de",
|
||||
"hover": "303134",
|
||||
"selected": "37393d",
|
||||
"attention": "e8b077",
|
||||
"live": "9ec7b2",
|
||||
"danger": "e39a90",
|
||||
"border_strong": "828386"
|
||||
"border_strong": "828386",
|
||||
"ocean_depth": "202123",
|
||||
"diff_add": "22392f",
|
||||
"diff_remove": "45302f"
|
||||
},
|
||||
"light": {
|
||||
"background": "faf8f5",
|
||||
"foreground": "28292b",
|
||||
"surface": "ffffff",
|
||||
"muted_foreground": "535451",
|
||||
"muted_foreground": "4f504d",
|
||||
"border": "d9d5cf",
|
||||
"sidebar": "f0ede8",
|
||||
"primary": "245bc7",
|
||||
"primary_foreground": "fbf5ee",
|
||||
"primary": "2e2f31",
|
||||
"primary_foreground": "faf8f5",
|
||||
"accent": "425c82",
|
||||
"hover": "e8e5e0",
|
||||
"selected": "dfdcd6",
|
||||
"attention": "86520d",
|
||||
"live": "3a6352",
|
||||
"danger": "9e3f36",
|
||||
"border_strong": "807c76"
|
||||
"border_strong": "807c76",
|
||||
"ocean_depth": "faf8f5",
|
||||
"diff_add": "d2eadd",
|
||||
"diff_remove": "f8e1dd"
|
||||
}
|
||||
},
|
||||
"typography": {
|
||||
@@ -81,8 +89,117 @@
|
||||
"reduced_poll_ms": 500,
|
||||
"duration_state_ms": 120,
|
||||
"duration_arrive_ms": 180,
|
||||
"duration_panel_ms": 340
|
||||
"duration_exit_ms": 120,
|
||||
"duration_panel_ms": 340,
|
||||
"duration_linger_ms": 1200,
|
||||
"arrive_rise_px": 4,
|
||||
"loop_beat_ms": 1200,
|
||||
"loop_breathe_ms": 2400,
|
||||
"loop_fps_cap": 15
|
||||
},
|
||||
"easing": {
|
||||
"out": [
|
||||
0,
|
||||
0,
|
||||
0.2,
|
||||
1
|
||||
],
|
||||
"in": [
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"panel": [
|
||||
0.2,
|
||||
0.1,
|
||||
0.16,
|
||||
1
|
||||
]
|
||||
},
|
||||
"stream": {
|
||||
"reveal_first_ms": 100,
|
||||
"reveal_lag_ms": 500,
|
||||
"rate_window_ms": 1000,
|
||||
"instant_graphemes": 8,
|
||||
"max_pending_bytes": 16384
|
||||
},
|
||||
"glow": {
|
||||
"blur_px": 6,
|
||||
"primary_alpha": 0.12,
|
||||
"live_alpha": 0.35,
|
||||
"attention_alpha": 0.4
|
||||
},
|
||||
"ombre": {
|
||||
"rise": "1e8fd8",
|
||||
"deep": "0b48bb"
|
||||
},
|
||||
"elevation": {
|
||||
"popover_y_px": 4,
|
||||
"popover_blur_px": 8,
|
||||
"popover_alpha_dark": 0.4,
|
||||
"popover_alpha_light": 0.16,
|
||||
"dialog_y_px": 12,
|
||||
"dialog_blur_px": 32,
|
||||
"dialog_alpha_dark": 0.5,
|
||||
"dialog_alpha_light": 0.2
|
||||
},
|
||||
"selection_opacity": 0.28,
|
||||
"primary_hover_opacity": 0.9
|
||||
"primary_hover_opacity": 0.9,
|
||||
"type_roles": {
|
||||
"caption": {
|
||||
"size_px": 12,
|
||||
"line_px": 16,
|
||||
"weight": 400
|
||||
},
|
||||
"label": {
|
||||
"size_px": 13,
|
||||
"line_px": 18,
|
||||
"weight": 400
|
||||
},
|
||||
"body": {
|
||||
"size_px": 14,
|
||||
"line_px": 22,
|
||||
"weight": 400
|
||||
},
|
||||
"prose": {
|
||||
"size_px": 16,
|
||||
"line_px": 26,
|
||||
"weight": 400
|
||||
},
|
||||
"heading": {
|
||||
"size_px": 17,
|
||||
"line_px": 24,
|
||||
"weight": 550
|
||||
},
|
||||
"title": {
|
||||
"size_px": 22,
|
||||
"line_px": 28,
|
||||
"weight": 550
|
||||
},
|
||||
"display": {
|
||||
"size_px": 28,
|
||||
"line_px": 34,
|
||||
"weight": 550
|
||||
},
|
||||
"code": {
|
||||
"size_px": 13,
|
||||
"line_px": 20,
|
||||
"weight": 400
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"control_px": 28,
|
||||
"row_px": 30,
|
||||
"touch_target_px": 44,
|
||||
"icon_px": 16,
|
||||
"icon_large_px": 20,
|
||||
"sidebar_px": 248,
|
||||
"panel_px": 360
|
||||
},
|
||||
"breakpoints": {
|
||||
"compact_px": 720,
|
||||
"medium_px": 1024,
|
||||
"wide_px": 1440
|
||||
}
|
||||
}
|
||||
Vendored
+82
-8
@@ -1,7 +1,7 @@
|
||||
// Generated from Codewhale GPUI design 1.1.2; sha256 433b18a41c526da0b5a1bbaa461c37feac987c7688fbe7de660acfbbdafa9bd5. Do not edit.
|
||||
// Generated from Codewhale GPUI design 2.3.0; sha256 9e74fb8f8fbfebb6b90a9740b324c56405277886e61273e2a0cb682797a8a6f9. Do not edit.
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub const VERSION: &str = "1.1.2";
|
||||
pub const VERSION: &str = "2.3.0";
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Colors {
|
||||
pub background: u32,
|
||||
@@ -12,44 +12,56 @@ pub struct Colors {
|
||||
pub sidebar: u32,
|
||||
pub primary: u32,
|
||||
pub primary_foreground: u32,
|
||||
pub accent: u32,
|
||||
pub hover: u32,
|
||||
pub selected: u32,
|
||||
pub attention: u32,
|
||||
pub live: u32,
|
||||
pub danger: u32,
|
||||
pub border_strong: u32,
|
||||
pub ocean_depth: u32,
|
||||
pub diff_add: u32,
|
||||
pub diff_remove: u32,
|
||||
}
|
||||
pub const DARK: Colors = Colors {
|
||||
background: 0x202123,
|
||||
foreground: 0xefeeeb,
|
||||
surface: 0x2a2b2e,
|
||||
muted_foreground: 0xc2c1bd,
|
||||
muted_foreground: 0xb8b8b4,
|
||||
border: 0x3b3c3f,
|
||||
sidebar: 0x191a1c,
|
||||
primary: 0x90b9ff,
|
||||
primary_foreground: 0x15243e,
|
||||
primary: 0xe6e4df,
|
||||
primary_foreground: 0x202123,
|
||||
accent: 0xa9c0de,
|
||||
hover: 0x303134,
|
||||
selected: 0x37393d,
|
||||
attention: 0xe8b077,
|
||||
live: 0x9ec7b2,
|
||||
danger: 0xe39a90,
|
||||
border_strong: 0x828386,
|
||||
ocean_depth: 0x202123,
|
||||
diff_add: 0x22392f,
|
||||
diff_remove: 0x45302f,
|
||||
};
|
||||
pub const LIGHT: Colors = Colors {
|
||||
background: 0xfaf8f5,
|
||||
foreground: 0x28292b,
|
||||
surface: 0xffffff,
|
||||
muted_foreground: 0x535451,
|
||||
muted_foreground: 0x4f504d,
|
||||
border: 0xd9d5cf,
|
||||
sidebar: 0xf0ede8,
|
||||
primary: 0x245bc7,
|
||||
primary_foreground: 0xfbf5ee,
|
||||
primary: 0x2e2f31,
|
||||
primary_foreground: 0xfaf8f5,
|
||||
accent: 0x425c82,
|
||||
hover: 0xe8e5e0,
|
||||
selected: 0xdfdcd6,
|
||||
attention: 0x86520d,
|
||||
live: 0x3a6352,
|
||||
danger: 0x9e3f36,
|
||||
border_strong: 0x807c76,
|
||||
ocean_depth: 0xfaf8f5,
|
||||
diff_add: 0xd2eadd,
|
||||
diff_remove: 0xf8e1dd,
|
||||
};
|
||||
pub fn colors(dark: bool) -> Colors {
|
||||
if dark { DARK } else { LIGHT }
|
||||
@@ -84,10 +96,72 @@ pub const MOTION_PET_POLL_MS: f32 = 30.0;
|
||||
pub const MOTION_REDUCED_POLL_MS: f32 = 500.0;
|
||||
pub const MOTION_DURATION_STATE_MS: f32 = 120.0;
|
||||
pub const MOTION_DURATION_ARRIVE_MS: f32 = 180.0;
|
||||
pub const MOTION_DURATION_EXIT_MS: f32 = 120.0;
|
||||
pub const MOTION_DURATION_PANEL_MS: f32 = 340.0;
|
||||
pub const MOTION_DURATION_LINGER_MS: f32 = 1200.0;
|
||||
pub const MOTION_ARRIVE_RISE_PX: f32 = 4.0;
|
||||
pub const MOTION_LOOP_BEAT_MS: f32 = 1200.0;
|
||||
pub const MOTION_LOOP_BREATHE_MS: f32 = 2400.0;
|
||||
pub const MOTION_LOOP_FPS_CAP: f32 = 15.0;
|
||||
pub const STREAM_REVEAL_FIRST_MS: f32 = 100.0;
|
||||
pub const STREAM_REVEAL_LAG_MS: f32 = 500.0;
|
||||
pub const STREAM_RATE_WINDOW_MS: f32 = 1000.0;
|
||||
pub const STREAM_INSTANT_GRAPHEMES: f32 = 8.0;
|
||||
pub const STREAM_MAX_PENDING_BYTES: f32 = 16384.0;
|
||||
pub const GLOW_BLUR_PX: f32 = 6.0;
|
||||
pub const GLOW_PRIMARY_ALPHA: f32 = 0.12;
|
||||
pub const GLOW_LIVE_ALPHA: f32 = 0.35;
|
||||
pub const GLOW_ATTENTION_ALPHA: f32 = 0.4;
|
||||
pub const ELEVATION_POPOVER_Y_PX: f32 = 4.0;
|
||||
pub const ELEVATION_POPOVER_BLUR_PX: f32 = 8.0;
|
||||
pub const ELEVATION_POPOVER_ALPHA_DARK: f32 = 0.4;
|
||||
pub const ELEVATION_POPOVER_ALPHA_LIGHT: f32 = 0.16;
|
||||
pub const ELEVATION_DIALOG_Y_PX: f32 = 12.0;
|
||||
pub const ELEVATION_DIALOG_BLUR_PX: f32 = 32.0;
|
||||
pub const ELEVATION_DIALOG_ALPHA_DARK: f32 = 0.5;
|
||||
pub const ELEVATION_DIALOG_ALPHA_LIGHT: f32 = 0.2;
|
||||
pub const EASING_OUT: [f32; 4] = [0.0, 0.0, 0.2, 1.0];
|
||||
pub const EASING_IN: [f32; 4] = [0.4, 0.0, 1.0, 1.0];
|
||||
pub const EASING_PANEL: [f32; 4] = [0.2, 0.1, 0.16, 1.0];
|
||||
pub const OMBRE_RISE: u32 = 0x1e8fd8;
|
||||
pub const OMBRE_DEEP: u32 = 0x0b48bb;
|
||||
pub const SELECTION_OPACITY: f32 = 0.28;
|
||||
pub const PRIMARY_HOVER_OPACITY: f32 = 0.9;
|
||||
pub const MONO_PX: f32 = 13.0;
|
||||
pub const TYPE_ROLE_CAPTION_SIZE_PX: f32 = 12.0;
|
||||
pub const TYPE_ROLE_CAPTION_LINE_PX: f32 = 16.0;
|
||||
pub const TYPE_ROLE_CAPTION_WEIGHT: f32 = 400.0;
|
||||
pub const TYPE_ROLE_LABEL_SIZE_PX: f32 = 13.0;
|
||||
pub const TYPE_ROLE_LABEL_LINE_PX: f32 = 18.0;
|
||||
pub const TYPE_ROLE_LABEL_WEIGHT: f32 = 400.0;
|
||||
pub const TYPE_ROLE_BODY_SIZE_PX: f32 = 14.0;
|
||||
pub const TYPE_ROLE_BODY_LINE_PX: f32 = 22.0;
|
||||
pub const TYPE_ROLE_BODY_WEIGHT: f32 = 400.0;
|
||||
pub const TYPE_ROLE_PROSE_SIZE_PX: f32 = 16.0;
|
||||
pub const TYPE_ROLE_PROSE_LINE_PX: f32 = 26.0;
|
||||
pub const TYPE_ROLE_PROSE_WEIGHT: f32 = 400.0;
|
||||
pub const TYPE_ROLE_HEADING_SIZE_PX: f32 = 17.0;
|
||||
pub const TYPE_ROLE_HEADING_LINE_PX: f32 = 24.0;
|
||||
pub const TYPE_ROLE_HEADING_WEIGHT: f32 = 550.0;
|
||||
pub const TYPE_ROLE_TITLE_SIZE_PX: f32 = 22.0;
|
||||
pub const TYPE_ROLE_TITLE_LINE_PX: f32 = 28.0;
|
||||
pub const TYPE_ROLE_TITLE_WEIGHT: f32 = 550.0;
|
||||
pub const TYPE_ROLE_DISPLAY_SIZE_PX: f32 = 28.0;
|
||||
pub const TYPE_ROLE_DISPLAY_LINE_PX: f32 = 34.0;
|
||||
pub const TYPE_ROLE_DISPLAY_WEIGHT: f32 = 550.0;
|
||||
pub const TYPE_ROLE_CODE_SIZE_PX: f32 = 13.0;
|
||||
pub const TYPE_ROLE_CODE_LINE_PX: f32 = 20.0;
|
||||
pub const TYPE_ROLE_CODE_WEIGHT: f32 = 400.0;
|
||||
pub const SIZE_CONTROL_PX: f32 = 28.0;
|
||||
pub const SIZE_ROW_PX: f32 = 30.0;
|
||||
pub const SIZE_TOUCH_TARGET_PX: f32 = 44.0;
|
||||
pub const SIZE_ICON_PX: f32 = 16.0;
|
||||
pub const SIZE_ICON_LARGE_PX: f32 = 20.0;
|
||||
pub const SIZE_SIDEBAR_PX: f32 = 248.0;
|
||||
pub const SIZE_PANEL_PX: f32 = 360.0;
|
||||
pub const BREAKPOINTS_COMPACT_PX: f32 = 720.0;
|
||||
pub const BREAKPOINTS_MEDIUM_PX: f32 = 1024.0;
|
||||
pub const BREAKPOINTS_WIDE_PX: f32 = 1440.0;
|
||||
pub const TYPE_BODY_PX: f32 = 14.0;
|
||||
pub const TYPE_CAPTION_PX: f32 = 12.0;
|
||||
pub const TYPE_PROSE_PX: f32 = 17.0;
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import Link from "next/link";
|
||||
import { Icon } from "@/components/icon";
|
||||
import { PageHeader, Section } from "@/components/page-header";
|
||||
import { Status } from "@/components/status-badge";
|
||||
import { getEnterprise } from "@/lib/i18n/dictionaries";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const copy = getEnterprise(locale);
|
||||
return buildPageMetadata({ path: "/enterprise", locale, title: copy.metaTitle, description: copy.metaDescription });
|
||||
}
|
||||
|
||||
export default async function EnterprisePage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const t = getEnterprise(locale);
|
||||
const mailto = `mailto:${t.partnerEmail}?subject=${encodeURIComponent("Codewhale Enterprise design partner")}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
kicker={t.kicker}
|
||||
title={t.title}
|
||||
lede={t.lead}
|
||||
pose="think"
|
||||
meta={<Status tone="attention">{t.status}</Status>}
|
||||
actions={
|
||||
<div className="actions">
|
||||
<a className="btn btn-primary btn-lg" href={mailto}>{t.partnerCta}</a>
|
||||
<Link className="btn btn-ghost" href={`/${locale}/install`}>{t.secondaryCta}</Link>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="page-body">
|
||||
<Section id="enterprise-loop" title={t.loopTitle} scope={t.loopScope}>
|
||||
<ol className="steps">
|
||||
{t.loop.map((step, index) => (
|
||||
<li key={step.title}>
|
||||
<span className="gs-step-index" aria-hidden="true">{index + 1}</span>
|
||||
<h3>{step.title}</h3>
|
||||
<p>{step.body}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Section>
|
||||
|
||||
<Section id="enterprise-status" title={t.statusTitle} scope={t.statusScope}>
|
||||
<div className="roadmap-tracks">
|
||||
{[
|
||||
{ title: t.todayTitle, tone: "ready" as const, items: t.today },
|
||||
{ title: t.plannedTitle, tone: "idle" as const, items: t.planned },
|
||||
].map((track) => (
|
||||
<section key={track.title} className="roadmap-track" aria-label={track.title}>
|
||||
<div className="roadmap-track-head">
|
||||
<h3><Status tone={track.tone}>{track.title}</Status></h3>
|
||||
</div>
|
||||
<ul className="group-card" role="list">
|
||||
{track.items.map((item) => (
|
||||
<li key={item.title} className="group-row roadmap-item">
|
||||
<p className="roadmap-item-title">{item.title}</p>
|
||||
<p className="roadmap-item-note">{item.body}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<section className="page-section" aria-label={t.controlTitle}>
|
||||
<div className="grid-2">
|
||||
<div className="tile">
|
||||
<span className="tile-icon" aria-hidden="true"><Icon name="folder" /></span>
|
||||
<h2 className="section-title">{t.dataTitle}</h2>
|
||||
<p>{t.dataBody}</p>
|
||||
</div>
|
||||
<div className="tile">
|
||||
<span className="tile-icon" aria-hidden="true"><Icon name="shield" /></span>
|
||||
<h2 className="section-title">{t.engineTitle}</h2>
|
||||
<p>{t.engineBody}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="page-section" aria-labelledby="enterprise-partners">
|
||||
<div className="callout">
|
||||
<p className="callout-title" id="enterprise-partners">
|
||||
<Icon name="users" className="icon" />
|
||||
{t.partnerTitle}
|
||||
</p>
|
||||
<p>{t.partnerBody}</p>
|
||||
<div className="actions">
|
||||
<a className="btn btn-secondary" href={mailto}>{t.partnerEmail}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -32,7 +32,8 @@ export default async function FeaturesPage({ params }: { params: Promise<{ local
|
||||
<Status tone={TONES[feature.status]}>{t[feature.status]}</Status>
|
||||
{Object.entries(feature.surfaces).map(([surface, state]) => <Status key={surface} tone={TONES[state]}><bdi>{surface.toUpperCase()}</bdi>: {t[state]}</Status>)}
|
||||
{feature.flag && <code dir="ltr">{feature.flag}</code>}
|
||||
{feature.since && (BUILD_FACTS.latestPublishedRelease && (compareVersions(feature.since, BUILD_FACTS.latestPublishedRelease.version) ?? 0) > 0
|
||||
{feature.since === "unreleased" && <Status tone="idle">{getHome(locale).sourceCandidate}</Status>}
|
||||
{feature.since && feature.since !== "unreleased" && (BUILD_FACTS.latestPublishedRelease && (compareVersions(feature.since, BUILD_FACTS.latestPublishedRelease.version) ?? 0) > 0
|
||||
? <Status tone="idle">{getHome(locale).sourceCandidate} <bdi>v{feature.since}</bdi></Status>
|
||||
: <code dir="ltr">v{feature.since}</code>)}
|
||||
</div>
|
||||
|
||||
@@ -103,11 +103,9 @@ export default async function LocaleLayout({
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: serializeJsonLd(siteJsonLd) }}
|
||||
/>
|
||||
{/* Site-wide theme, applied before paint so there is no flash. With
|
||||
no pin, the stylesheet's `prefers-color-scheme` rules resolve the
|
||||
OS appearance (and track it live); a stored "light" or "dark"
|
||||
(`cw-theme`, see components/theme-toggle.tsx) pins the scheme.
|
||||
"system", a legacy "auto", or no choice leaves it to the OS. */}
|
||||
{/* Site-wide theme, applied before paint so there is no flash. The
|
||||
site is light unless a stored "dark" (`cw-theme`, see
|
||||
components/theme-toggle.tsx) pins the dark set. */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
|
||||
+66
-62
@@ -35,8 +35,9 @@ const AVAILABILITY_TONES: StatusTone[] = ["ready", "ready", "idle", "idle"];
|
||||
/**
|
||||
* The whale-road homepage: the promise, capabilities and reading
|
||||
* sections sit on paper. The live v2 whale (the desktop app's own Director)
|
||||
* rests beside the hero, and the page ends in the sea with the install
|
||||
* command, running into the footer.
|
||||
* rests on the top edge of a real terminal capture just under the promise,
|
||||
* and the page ends in the sea with the install command, running into the
|
||||
* footer.
|
||||
*
|
||||
* One memorable thing moves: the whale. It breathes, glances toward the
|
||||
* pointer, and performs its existing poses. Everything else is still.
|
||||
@@ -75,7 +76,7 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
|
||||
/>
|
||||
|
||||
{/* PAPER — the promise, one primary action, the install command, and
|
||||
the whale resting on the horizon. */}
|
||||
the whale resting on the terminal below. */}
|
||||
<section className="home-hero" aria-labelledby="home-title">
|
||||
<div className="home-hero-inner">
|
||||
<div className="home-hero-copy">
|
||||
@@ -105,8 +106,23 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<figure className="home-terminal figure">
|
||||
<div className="figure-frame" dir="ltr">
|
||||
<TerminalCapture
|
||||
frame="home"
|
||||
regionLabel={d.shotPreview}
|
||||
label={fill(d.screenshotAlt, { version: TERMINAL_SCREENSHOT.version })}
|
||||
/>
|
||||
</div>
|
||||
<figcaption className="figure-caption">
|
||||
{TERMINAL_SCREENSHOT.version === publishedRelease?.version
|
||||
? `${d.shotPreview} · v${TERMINAL_SCREENSHOT.version}`
|
||||
: `${d.shotPreview} · ${d.sourceCandidate} v${TERMINAL_SCREENSHOT.version}`}
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<div
|
||||
className="home-release status-line"
|
||||
className="home-proof"
|
||||
data-source-state={sourceIsPublished ? "published release" : "source candidate"}
|
||||
data-source-state-label={sourceIsPublished ? d.publishedRelease : d.sourceCandidate}
|
||||
>
|
||||
@@ -117,68 +133,11 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
|
||||
</Status>
|
||||
<span>{`${sourceIsPublished ? d.currentSource : d.sourceCandidate} v${sourceVersion}`}</span>
|
||||
<span>{facts.license ?? "MIT"}</span>
|
||||
<span>{fill(d.proofContributors, { count: facts.contributorCount })}</span>
|
||||
</div>
|
||||
|
||||
{showPromo && (
|
||||
<section className="home-promo" aria-labelledby="home-promo-title">
|
||||
<a href={PARTNER_PROMO.href} rel="sponsored" className="home-promo-card">
|
||||
<span className="figure-frame">
|
||||
<Image
|
||||
src={PARTNER_PROMO.image.src}
|
||||
alt={pickText(PARTNER_PROMO.image.alt, locale)}
|
||||
width={PARTNER_PROMO.image.width}
|
||||
height={PARTNER_PROMO.image.height}
|
||||
unoptimized
|
||||
/>
|
||||
</span>
|
||||
<span className="home-promo-text">
|
||||
<span className="home-promo-title" id="home-promo-title">{PARTNER_PROMO.title}</span>
|
||||
<span className="home-promo-body">{pickText(PARTNER_PROMO.body, locale)}</span>
|
||||
<span className="home-promo-body">{pickText(PARTNER_PROMO.disclosure, locale)}</span>
|
||||
<span className="section-link">
|
||||
{pickText(PARTNER_PROMO.cta, locale)}
|
||||
<Icon name="external" />
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<figure className="home-terminal figure">
|
||||
<div className="figure-frame" dir="ltr">
|
||||
<TerminalCapture
|
||||
frame="home"
|
||||
regionLabel={d.shotPreview}
|
||||
label={fill(d.screenshotAlt, { version: TERMINAL_SCREENSHOT.version })}
|
||||
/>
|
||||
</div>
|
||||
<figcaption className="figure-caption">
|
||||
{d.shotPreview} · {d.sourceCandidate} v{TERMINAL_SCREENSHOT.version}
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
{/* PAPER — models, connections, and the getting-started path. */}
|
||||
<div className="home-body">
|
||||
{showHighlights && (
|
||||
<Section
|
||||
id="home-release-highlights"
|
||||
layout="split"
|
||||
className="home-section"
|
||||
title={fill(pickText(highlightsPublished ? RELEASE_HIGHLIGHTS.newHeading : RELEASE_HIGHLIGHTS.comingHeading, locale), { version: RELEASE_HIGHLIGHTS.version })}
|
||||
scope={highlightsPublished ? undefined : pickText(RELEASE_HIGHLIGHTS.comingLede, locale)}
|
||||
link={<Link href={`/${locale}/changelog`} className="section-link">{pickText(RELEASE_HIGHLIGHTS.changelogLink, locale)}</Link>}
|
||||
>
|
||||
<dl className="ruled-list">
|
||||
{RELEASE_HIGHLIGHTS.items.map((item) => (
|
||||
<div key={item.title}>
|
||||
<dt>{item.title}</dt>
|
||||
<dd>{pickText(item.body, locale)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section
|
||||
id="home-capabilities"
|
||||
layout="split"
|
||||
@@ -220,6 +179,31 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
|
||||
</dl>
|
||||
</Section>
|
||||
|
||||
{showPromo && (
|
||||
<section className="home-section home-promo" aria-labelledby="home-promo-title">
|
||||
<a href={PARTNER_PROMO.href} rel="sponsored" className="home-promo-card">
|
||||
<span className="figure-frame">
|
||||
<Image
|
||||
src={PARTNER_PROMO.image.src}
|
||||
alt={pickText(PARTNER_PROMO.image.alt, locale)}
|
||||
width={PARTNER_PROMO.image.width}
|
||||
height={PARTNER_PROMO.image.height}
|
||||
unoptimized
|
||||
/>
|
||||
</span>
|
||||
<span className="home-promo-text">
|
||||
<span className="home-promo-title" id="home-promo-title">{PARTNER_PROMO.title}</span>
|
||||
<span className="home-promo-body">{pickText(PARTNER_PROMO.body, locale)}</span>
|
||||
<span className="home-promo-body">{pickText(PARTNER_PROMO.disclosure, locale)}</span>
|
||||
<span className="section-link">
|
||||
{pickText(PARTNER_PROMO.cta, locale)}
|
||||
<Icon name="external" />
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Section
|
||||
id="home-surfaces"
|
||||
layout="split"
|
||||
@@ -284,6 +268,26 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
|
||||
<p className="section-scope mt-4">{d.availabilityNote}</p>
|
||||
</Section>
|
||||
|
||||
{showHighlights && (
|
||||
<Section
|
||||
id="home-release-highlights"
|
||||
layout="split"
|
||||
className="home-section"
|
||||
title={fill(pickText(highlightsPublished ? RELEASE_HIGHLIGHTS.newHeading : RELEASE_HIGHLIGHTS.comingHeading, locale), { version: RELEASE_HIGHLIGHTS.version })}
|
||||
scope={highlightsPublished ? undefined : pickText(RELEASE_HIGHLIGHTS.comingLede, locale)}
|
||||
link={<Link href={`/${locale}/changelog`} className="section-link">{pickText(RELEASE_HIGHLIGHTS.changelogLink, locale)}</Link>}
|
||||
>
|
||||
<dl className="ruled-list">
|
||||
{RELEASE_HIGHLIGHTS.items.map((item) => (
|
||||
<div key={item.title}>
|
||||
<dt>{item.title}</dt>
|
||||
<dd>{pickText(item.body, locale)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* COMMUNITY */}
|
||||
<section className="home-section" aria-labelledby="home-community">
|
||||
<div className="section-split">
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { readCatalogue } from "@/lib/ratatui/catalogue";
|
||||
|
||||
// Public, indexable routes (locale-prefixed). /admin and /api are
|
||||
// intentionally excluded; see app/robots.ts.
|
||||
const PATHS = ["", "/product", "/features", "/install", "/constitution", "/models", "/plugins", "/runtime", "/docs", "/docs/auth", "/docs/configuration", "/docs/guide", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/review", "/docs/fleet", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/trust", "/docs/vocabulary", "/docs/web", "/docs/work", "/docs/skills", "/docs/claude-code", "/docs/pet", "/faq", "/roadmap", "/feed", "/digest", "/changelog", "/contribute", "/community", "/signin", "/legal/terms", "/legal/privacy"];
|
||||
const PATHS = ["", "/product", "/features", "/install", "/constitution", "/models", "/plugins", "/runtime", "/docs", "/docs/auth", "/docs/configuration", "/docs/guide", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/review", "/docs/fleet", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/trust", "/docs/vocabulary", "/docs/web", "/docs/work", "/docs/skills", "/docs/claude-code", "/docs/pet", "/faq", "/roadmap", "/enterprise", "/feed", "/digest", "/changelog", "/contribute", "/community", "/signin", "/legal/terms", "/legal/privacy"];
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const catalogue = readCatalogue();
|
||||
|
||||
+32
-25
@@ -3,8 +3,9 @@
|
||||
/* ------------------------------------------------------------------ */
|
||||
/*
|
||||
* Paper holds the promise, capabilities and reading sections.
|
||||
* The live whale stays beside the hero. The page ends in the sea with the
|
||||
* install command, which runs on into the footer.
|
||||
* The live whale rests on the top edge of the real terminal capture under
|
||||
* the hero. The page ends in the sea with the install command, which runs
|
||||
* on into the footer.
|
||||
*/
|
||||
|
||||
.home { --home-measure: 36rem; }
|
||||
@@ -12,7 +13,7 @@
|
||||
/* ---------- paper: the hero ---------- */
|
||||
.home-hero {
|
||||
position: relative;
|
||||
padding-block: clamp(3.5rem, 9vw, 7.5rem) 0;
|
||||
padding-block: clamp(2.5rem, 6vw, 4.5rem) 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
@@ -28,7 +29,7 @@
|
||||
.home-hero-copy {
|
||||
display: grid;
|
||||
gap: var(--space-5);
|
||||
padding-bottom: clamp(3rem, 7vw, 5.5rem);
|
||||
padding-bottom: clamp(2rem, 4vw, 3rem);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -37,7 +38,7 @@
|
||||
English; hyphenation lets German compounds break at syllables. */
|
||||
.home-title {
|
||||
max-width: 9.5em;
|
||||
font-size: clamp(2.5rem, 1.4rem + 4.4vw, 5rem);
|
||||
font-size: clamp(2.5rem, 1.4rem + 3.6vw, 4.5rem);
|
||||
hyphens: auto;
|
||||
font-weight: 650;
|
||||
line-height: 0.98;
|
||||
@@ -73,7 +74,8 @@
|
||||
.home-hero-copy .hero-install { max-width: 32rem; margin-top: var(--space-2); }
|
||||
.home-hero-copy .actions { margin-top: calc(var(--space-2) * -1); }
|
||||
|
||||
/* The whale rests on the horizon: its lowest point meets the line. */
|
||||
/* The whale rests on the terminal: its lowest point meets the frame's top
|
||||
edge. */
|
||||
.home-whale {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -81,10 +83,10 @@
|
||||
min-width: 0;
|
||||
}
|
||||
.home-whale .whale-live {
|
||||
--whale-size: clamp(18rem, 40vw, 34rem);
|
||||
--whale-size: clamp(16rem, 30vw, 26rem);
|
||||
width: var(--whale-size);
|
||||
/* The rig fills the middle 72% of the canvas; drop its belly onto the
|
||||
waterline where the tide begins. */
|
||||
terminal frame below. */
|
||||
margin-bottom: calc(var(--whale-size) * -0.26);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
@@ -159,29 +161,34 @@
|
||||
background: var(--horizon-light);
|
||||
}
|
||||
|
||||
/* ---------- release and source status ---------- */
|
||||
.home-release {
|
||||
/* ---------- the product, then what backs it ---------- */
|
||||
.home-terminal {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: var(--container);
|
||||
margin-inline: auto;
|
||||
}
|
||||
.home-terminal .figure-frame {
|
||||
box-shadow: 0 1px 0 var(--line), 0 32px 64px -32px rgb(10 30 51 / 0.5);
|
||||
}
|
||||
|
||||
/* Release, licence and the contributor count read from facts, never typed into copy. */
|
||||
.home-proof {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-2) var(--space-6);
|
||||
width: var(--container);
|
||||
margin-inline: auto;
|
||||
padding-block: var(--space-5);
|
||||
border-block: 1px solid var(--line);
|
||||
border-bottom: 1px solid var(--line);
|
||||
color: var(--muted);
|
||||
font-size: var(--text-ui);
|
||||
}
|
||||
|
||||
.home-terminal {
|
||||
width: var(--container);
|
||||
max-width: 1000px;
|
||||
margin-inline: auto;
|
||||
padding-block-start: var(--space-6);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ---------- partner offer ---------- */
|
||||
.home-promo {
|
||||
width: var(--container);
|
||||
max-width: 1000px;
|
||||
margin-inline: auto;
|
||||
padding-block-start: var(--space-6);
|
||||
}
|
||||
.home-promo { max-width: 1000px; }
|
||||
.home-promo-card {
|
||||
display: grid;
|
||||
gap: var(--space-4) var(--space-6);
|
||||
@@ -245,7 +252,7 @@
|
||||
.home-hero-inner { grid-template-columns: minmax(0, 1fr); }
|
||||
.home-hero-copy { padding-bottom: var(--space-4); }
|
||||
.home-whale { justify-content: flex-end; }
|
||||
.home-whale .whale-live { --whale-size: clamp(14rem, 70vw, 20rem); margin-inline: auto; }
|
||||
.home-whale .whale-live { --whale-size: clamp(11rem, 48vw, 16rem); }
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
|
||||
@@ -588,6 +588,17 @@ a.dir-row:hover .dir-action { color: var(--text); }
|
||||
.code .code-copy:focus-visible { box-shadow: var(--glow-focus); }
|
||||
.code .code-copy:hover { background: var(--hover); color: var(--text); }
|
||||
.code-copy[data-copied="true"] { color: var(--live); }
|
||||
@media (max-width: 30rem) {
|
||||
.code pre.code-block { padding-right: 3rem; }
|
||||
.code .code-copy-label {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- plate ---------- */
|
||||
/* The install plate: a raised sheet that holds a choice, a command and its
|
||||
|
||||
+149
-185
@@ -2,92 +2,47 @@
|
||||
/* Tokens and roles — the whale-road */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/*
|
||||
* Direction: codewhale-design/DIRECTION.md. Colour authority: the Moda brand
|
||||
* kit "Codewhale" (--kit-* below, mapping in docs/BRAND_COLOR_MAPPING.md).
|
||||
* Geometry, radii and focus metrics still come from the generated
|
||||
* `--gpui-*` tokens in ../tokens.css. This file names the roles every page
|
||||
* paints with.
|
||||
* Direction: codewhale-design/DIRECTION.md. Authority: codewhale-design
|
||||
* SYSTEM.md and tokens.json, vendored at ../../vendor/codewhale-design and
|
||||
* exported to ../tokens.css (`--gpui-*`) by scripts/export-design-tokens.py.
|
||||
* This file only names the roles every page paints with; every value is a
|
||||
* generated token, so the site, the desktop app and the web app share one
|
||||
* palette, type scale, radius set and motion.
|
||||
*
|
||||
* The ocean. Dark is the preferred look: deep navy grounds that rise into
|
||||
* the logo's gradient (#1E8FD8 -> #0B48BB). Light is warm paper above one
|
||||
* calm horizon. Below the horizon (the footer, the home sea) is always
|
||||
* the deep water, in both appearances (`.stage`).
|
||||
* Grounds are pearl (light) and charcoal (dark), the desktop shell's own.
|
||||
* Filled actions are neutral ink (`primary`); one quiet slate (`accent`)
|
||||
* marks links, focus and selection. The logo ombre lives on the whale.
|
||||
*
|
||||
* Depth is elevation. --panel is raised (nearer the surface, lighter);
|
||||
* --bg is the floor; --surface is recessed (code, rails, fields).
|
||||
*
|
||||
* Light means something. Focus, live work and "needs you" carry a small
|
||||
* point of light (--glow). Nothing else glows.
|
||||
*
|
||||
* State is a mark and a word: --live (working, done, ready), --attention
|
||||
* (needs you), --danger (failed), --muted (idle). Never colour alone.
|
||||
*
|
||||
* Text, accent and state inks are kit colours in both appearances, and the
|
||||
* navy grounds are kit colours too; they are measured against every ink
|
||||
* here (gpui-role-tokens.test.ts checks WCAG AA on every ground).
|
||||
*
|
||||
* NAMING TRAP: tokens.json `surface` is the RAISED colour (web --panel);
|
||||
* tokens.json `sidebar` is the RECESSED one (web --surface).
|
||||
*/
|
||||
|
||||
/* ---------- scheme-independent tokens ---------- */
|
||||
:root {
|
||||
/* The ocean: the navy grounds and the logo gradient. */
|
||||
--kit-whale-blue: #0b48bb;
|
||||
--kit-current-blue: #1e8fd8;
|
||||
--kit-mark-navy: #142352;
|
||||
--kit-abyss: #0f1e2d;
|
||||
--kit-deep-navy: #142638;
|
||||
--kit-ocean-depth: #173b50;
|
||||
--kit-night-surface: #20374e;
|
||||
--kit-night-border: #31495e;
|
||||
--kit-foam: #e6eef7;
|
||||
--kit-mist: #bbc9d7;
|
||||
--kit-bioluminescence: #8fbdff;
|
||||
--kit-warm-paper: #faf9f5;
|
||||
--kit-white: #ffffff;
|
||||
--kit-seaglass: #edf1ef;
|
||||
--kit-shallows: #dcefeb;
|
||||
--kit-tideline: #d2dedd;
|
||||
--kit-ink: #263844;
|
||||
--kit-slate: #495a63;
|
||||
--kit-signal-blue: #245bc7;
|
||||
--kit-kelp: #9ecbbd;
|
||||
--kit-kelp-deep: #396554;
|
||||
--kit-lantern: #eab982;
|
||||
--kit-amber: #84530d;
|
||||
--kit-coral: #eea59d;
|
||||
--kit-rust: #9e3f36;
|
||||
--ocean-abyss: var(--kit-abyss);
|
||||
--ocean-floor: var(--kit-deep-navy);
|
||||
--ocean-raised: var(--kit-ocean-depth);
|
||||
--ocean-hover: var(--kit-night-surface);
|
||||
--ocean-selected: var(--kit-night-border);
|
||||
--ocean-line: var(--kit-night-border);
|
||||
--ocean-line-strong: var(--kit-mist);
|
||||
--ocean-sky: var(--kit-mark-navy);
|
||||
--brand-light: var(--kit-current-blue);
|
||||
--brand-deep: var(--kit-whale-blue);
|
||||
--brand-mid: var(--kit-signal-blue);
|
||||
/* The primary fill: Signal Blue into Whale Blue; white text clears 6:1. */
|
||||
--brand-fill: linear-gradient(135deg, var(--brand-mid), var(--brand-deep));
|
||||
--brand-fill-hover: linear-gradient(135deg, var(--brand-deep), var(--kit-mark-navy));
|
||||
--on-brand: var(--kit-white);
|
||||
/* The sea, one ramp from the logo's deep blue at the surface to the
|
||||
floor. It settles within a few rows so text from 3rem down sits on
|
||||
>= 4.5:1 muted contrast; it is never a bright wall. */
|
||||
--sea-surface: var(--brand-deep);
|
||||
--sea-shallow: #0f3991;
|
||||
--sea-mid: var(--kit-mark-navy);
|
||||
--sea: linear-gradient(180deg, var(--sea-shallow) 0, var(--sea-mid) 5rem, var(--ocean-floor) 22rem);
|
||||
--sea-band: linear-gradient(180deg, var(--sea-shallow) 0, var(--sea-mid) 40%, var(--ocean-floor) 100%);
|
||||
/* The tide (the merch's Tide ombré): paper, light aqua, cobalt, then the
|
||||
deep. Headings start past the logo-deep stop (7rem), where text ink
|
||||
clears 4.5:1; muted ink only appears past the mid stop (11rem). */
|
||||
--tide-aqua: var(--kit-shallows);
|
||||
--tide: linear-gradient(180deg, var(--kit-warm-paper) 0, var(--tide-aqua) 2rem, var(--brand-light) 4.5rem, var(--brand-deep) 7rem, var(--sea-mid) 11rem, var(--ocean-floor) 26rem);
|
||||
/* The horizon: one line of light where the paper meets the sea. */
|
||||
--horizon-light: rgb(143 189 255 / 0.85);
|
||||
/* Below the horizon (the footer, the home install band) is the dark set
|
||||
in both appearances. */
|
||||
--ocean-abyss: var(--gpui-dark-sidebar);
|
||||
--ocean-floor: var(--gpui-dark-ocean-depth);
|
||||
--ocean-raised: var(--gpui-dark-panel);
|
||||
--ocean-hover: var(--gpui-dark-hover);
|
||||
--ocean-selected: var(--gpui-dark-list-active);
|
||||
--ocean-line: var(--gpui-dark-border);
|
||||
--ocean-line-strong: var(--gpui-dark-border-strong);
|
||||
--ocean-sky: var(--gpui-dark-ocean-depth);
|
||||
/* The logo ombre, for the whale and the mark only. */
|
||||
--brand-light: #1e8fd8;
|
||||
--brand-deep: #0b48bb;
|
||||
--ombre: var(--gpui-ombre);
|
||||
--sea: linear-gradient(var(--ocean-floor), var(--ocean-floor));
|
||||
--sea-band: var(--sea);
|
||||
/* The horizon: one quiet line of accent where the page meets the floor. */
|
||||
--horizon-light: rgb(var(--gpui-dark-accent-rgb) / 0.6);
|
||||
--horizon-glow: var(--horizon-light);
|
||||
|
||||
/* Type: Shannon Sans for every role, the system monospace stack for
|
||||
@@ -99,29 +54,32 @@
|
||||
"Liberation Mono", monospace;
|
||||
--font-cjk: var(--gpui-font-fallbacks), sans-serif;
|
||||
|
||||
/* Type scale. 12px is the floor. Display sizes are one per page. */
|
||||
--text-caption: 0.75rem; /* 12 meta, timestamps, labels */
|
||||
--text-mono: var(--gpui-mono-size); /* 13 code, commands */
|
||||
--text-ui: 0.875rem; /* 14 controls, rows, panel body */
|
||||
--text-body: 1rem; /* 16 short copy, form inputs (no iOS zoom) */
|
||||
--text-prose: 1.0625rem; /* 17 reading: docs, legal, answers */
|
||||
--leading-prose: 1.6;
|
||||
--text-heading: 1.0625rem; /* 17 600 group heading */
|
||||
--text-title: 1.375rem; /* 22 600 group and prose title */
|
||||
--text-section: clamp(1.75rem, 1.2rem + 1.7vw, 2.5rem); /* 28-40 editorial section title */
|
||||
--text-display: clamp(2rem, 1.3rem + 2.6vw, 3rem); /* inner-page title */
|
||||
--text-display-lg: clamp(2.5rem, 1.4rem + 4.4vw, 4.5rem); /* the home hero */
|
||||
/* Type roles from tokens.json. The editorial section and display sizes
|
||||
are the website's own layout: fluid steps above the token display. */
|
||||
--text-caption: var(--gpui-type-caption-size);
|
||||
--text-mono: var(--gpui-type-code-size);
|
||||
--text-label: var(--gpui-type-label-size);
|
||||
--text-ui: var(--gpui-type-body-size);
|
||||
--text-body: var(--gpui-type-prose-size);
|
||||
--text-prose: var(--gpui-type-prose-size);
|
||||
--leading-prose: var(--gpui-type-prose-line);
|
||||
--text-heading: var(--gpui-type-heading-size);
|
||||
--text-title: var(--gpui-type-title-size);
|
||||
--text-section: clamp(var(--gpui-type-display-size), 1.2rem + 1.7vw, 2.5rem);
|
||||
--text-display: clamp(2rem, 1.3rem + 2.6vw, 3rem);
|
||||
--text-display-lg: clamp(2.5rem, 1.4rem + 4.4vw, 4.5rem);
|
||||
--weight-heading: var(--gpui-type-heading-weight);
|
||||
--text-xs: var(--text-caption);
|
||||
--text-sm: var(--text-ui);
|
||||
--text-base: var(--text-body);
|
||||
|
||||
/* Space: a 4px program. */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-5: 1.5rem;
|
||||
--space-6: 2rem;
|
||||
/* Space: the token steps (4, 8, 12, 16, 24, 32), then page rhythm. */
|
||||
--space-1: var(--gpui-spacing-compact);
|
||||
--space-2: var(--gpui-spacing-control);
|
||||
--space-3: var(--gpui-spacing-group);
|
||||
--space-4: var(--gpui-spacing-section);
|
||||
--space-5: var(--gpui-spacing-large);
|
||||
--space-6: var(--gpui-spacing-page);
|
||||
--space-7: 3rem;
|
||||
--space-8: 4rem;
|
||||
--section-gap: clamp(var(--space-7), 7vw, 5.5rem);
|
||||
@@ -133,21 +91,21 @@
|
||||
--hero-pad: clamp(3rem, 7vw, 5.5rem);
|
||||
--section-pad: clamp(2.75rem, 5vw, 4rem);
|
||||
|
||||
/* Radius: 6 controls, 10 surfaces, 14 sheets, pill for dots. */
|
||||
/* Radius: control, panel, sheet; pill for dots. */
|
||||
--radius-control: var(--gpui-radius-control);
|
||||
--radius-surface: var(--gpui-radius-panel);
|
||||
--radius-sheet: var(--gpui-radius-sheet);
|
||||
--radius-pill: 999px;
|
||||
|
||||
/* Motion. Nothing bounces: every curve stays inside [0, 1]. */
|
||||
--dur-state: 120ms; /* colour, border, opacity on hover and press */
|
||||
--dur-arrive: 180ms; /* something new arrives, rise <= 4px */
|
||||
--dur-exit: 120ms; /* something closes or leaves */
|
||||
--dur-spring: 340ms; /* panes and sheets */
|
||||
--dur-spin: 1.2s; /* a working spinner: motion that shows work */
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--ease-spring: cubic-bezier(0.2, 0.1, 0.16, 1);
|
||||
/* Motion from tokens.json. Nothing bounces: every curve stays in [0, 1]. */
|
||||
--dur-state: var(--gpui-motion-duration-state);
|
||||
--dur-arrive: var(--gpui-motion-duration-arrive);
|
||||
--dur-exit: var(--gpui-motion-duration-exit);
|
||||
--dur-spring: var(--gpui-motion-duration-panel);
|
||||
--dur-spin: var(--gpui-motion-loop-beat);
|
||||
--ease-out: var(--gpui-easing-out);
|
||||
--ease-in: var(--gpui-easing-in);
|
||||
--ease-spring: var(--gpui-easing-panel);
|
||||
--ease-exit: var(--ease-in);
|
||||
}
|
||||
|
||||
@@ -163,104 +121,110 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- light — warm paper above the horizon ---------- */
|
||||
/* ---------- light — pearl ---------- */
|
||||
:root {
|
||||
--bg: var(--kit-warm-paper);
|
||||
--surface: var(--kit-seaglass);
|
||||
--panel: var(--kit-white);
|
||||
--text: var(--kit-ink);
|
||||
--muted: var(--kit-slate);
|
||||
--line: var(--kit-tideline);
|
||||
--line-strong: var(--kit-slate);
|
||||
--accent: var(--kit-signal-blue);
|
||||
--accent-hover: rgb(36 91 199 / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--kit-white);
|
||||
--hover: var(--kit-seaglass);
|
||||
--selected: var(--kit-shallows);
|
||||
--selection: rgb(36 91 199 / var(--gpui-selection-opacity));
|
||||
--ring: var(--kit-signal-blue);
|
||||
--live: var(--kit-kelp-deep);
|
||||
--success: var(--kit-kelp-deep);
|
||||
--attention: var(--kit-amber);
|
||||
--danger: var(--kit-rust);
|
||||
--mark-ink: var(--kit-ink);
|
||||
--ombre-ink: linear-gradient(135deg, var(--brand-light), var(--brand-deep));
|
||||
--diff-add: rgb(57 101 84 / 0.14);
|
||||
--diff-del: rgb(158 63 54 / 0.11);
|
||||
/* Paper: the plain ground above the horizon. */
|
||||
--ground: var(--kit-warm-paper);
|
||||
/* On paper, light is a quiet ring. */
|
||||
--bg: var(--gpui-light-bg);
|
||||
--surface: var(--gpui-light-sidebar);
|
||||
--panel: var(--gpui-light-panel);
|
||||
--text: var(--gpui-light-text);
|
||||
--muted: var(--gpui-light-text-muted);
|
||||
--line: var(--gpui-light-border);
|
||||
--line-strong: var(--gpui-light-border-strong);
|
||||
--accent: var(--gpui-light-accent);
|
||||
--accent-hover: rgb(var(--gpui-light-accent-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--gpui-light-on-primary);
|
||||
--brand-fill: var(--gpui-light-primary);
|
||||
--brand-fill-hover: rgb(var(--gpui-light-primary-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-brand: var(--gpui-light-on-primary);
|
||||
--hover: var(--gpui-light-hover);
|
||||
--selected: var(--gpui-light-list-active);
|
||||
--selection: rgb(var(--gpui-light-accent-rgb) / var(--gpui-selection-opacity));
|
||||
--ring: var(--gpui-light-accent);
|
||||
--live: var(--gpui-light-live);
|
||||
--success: var(--gpui-light-live);
|
||||
--attention: var(--gpui-light-attention);
|
||||
--danger: var(--gpui-light-danger);
|
||||
--mark-ink: var(--gpui-light-text);
|
||||
--ombre-ink: var(--ombre);
|
||||
--diff-add: var(--gpui-light-diff-add);
|
||||
--diff-del: var(--gpui-light-diff-remove);
|
||||
--ground: var(--gpui-light-bg);
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 16%, transparent);
|
||||
--glow-focus: 0 0 0 6px rgb(36 91 199 / 0.12);
|
||||
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.16);
|
||||
--glow-focus: 0 0 0 4px rgb(var(--gpui-light-accent-rgb) / 0.16);
|
||||
--shadow-popover: var(--gpui-elevation-popover-light);
|
||||
--shadow-dialog: var(--gpui-elevation-dialog-light);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* ---------- dark — the navy ocean, only when pinned ---------- */
|
||||
/* Paper is the site's appearance; the sea lives below the horizon in both.
|
||||
A reader who pins dark from the theme control gets the navy ground. */
|
||||
/* ---------- dark — charcoal, only when pinned ---------- */
|
||||
:root[data-theme="dark"] {
|
||||
--bg: var(--ocean-floor);
|
||||
--surface: var(--ocean-abyss);
|
||||
--panel: var(--ocean-raised);
|
||||
--text: var(--kit-foam);
|
||||
--muted: var(--kit-mist);
|
||||
--line: var(--ocean-line);
|
||||
--line-strong: var(--ocean-line-strong);
|
||||
--accent: var(--kit-bioluminescence);
|
||||
--accent-hover: rgb(143 189 255 / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--kit-abyss);
|
||||
--hover: var(--ocean-hover);
|
||||
--selected: var(--ocean-selected);
|
||||
--selection: rgb(143 189 255 / var(--gpui-selection-opacity));
|
||||
--ring: var(--kit-bioluminescence);
|
||||
--live: var(--kit-kelp);
|
||||
--success: var(--kit-kelp);
|
||||
--attention: var(--kit-lantern);
|
||||
--danger: var(--kit-coral);
|
||||
--mark-ink: var(--kit-foam);
|
||||
--ombre-ink: linear-gradient(135deg, var(--kit-bioluminescence), var(--brand-light));
|
||||
--diff-add: rgb(158 203 189 / 0.16);
|
||||
--diff-del: rgb(238 165 157 / 0.12);
|
||||
--ground: radial-gradient(90rem 38rem at 50% -14rem, rgb(30 143 216 / 0.3), transparent 70%), linear-gradient(180deg, var(--ocean-sky) 0, var(--ocean-floor) 34rem);
|
||||
--bg: var(--gpui-dark-bg);
|
||||
--surface: var(--gpui-dark-sidebar);
|
||||
--panel: var(--gpui-dark-panel);
|
||||
--text: var(--gpui-dark-text);
|
||||
--muted: var(--gpui-dark-text-muted);
|
||||
--line: var(--gpui-dark-border);
|
||||
--line-strong: var(--gpui-dark-border-strong);
|
||||
--accent: var(--gpui-dark-accent);
|
||||
--accent-hover: rgb(var(--gpui-dark-accent-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--gpui-dark-on-primary);
|
||||
--brand-fill: var(--gpui-dark-primary);
|
||||
--brand-fill-hover: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-brand: var(--gpui-dark-on-primary);
|
||||
--hover: var(--gpui-dark-hover);
|
||||
--selected: var(--gpui-dark-list-active);
|
||||
--selection: rgb(var(--gpui-dark-accent-rgb) / var(--gpui-selection-opacity));
|
||||
--ring: var(--gpui-dark-accent);
|
||||
--live: var(--gpui-dark-live);
|
||||
--success: var(--gpui-dark-live);
|
||||
--attention: var(--gpui-dark-attention);
|
||||
--danger: var(--gpui-dark-danger);
|
||||
--mark-ink: var(--gpui-dark-text);
|
||||
--ombre-ink: var(--ombre);
|
||||
--diff-add: var(--gpui-dark-diff-add);
|
||||
--diff-del: var(--gpui-dark-diff-remove);
|
||||
--ground: var(--gpui-dark-bg);
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 20%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 45%, transparent);
|
||||
--glow-focus: 0 0 14px 2px rgb(143 189 255 / 0.35);
|
||||
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.4);
|
||||
--glow-focus: 0 0 0 4px rgb(var(--gpui-dark-accent-rgb) / 0.3);
|
||||
--shadow-popover: var(--gpui-elevation-popover-dark);
|
||||
--shadow-dialog: var(--gpui-elevation-dialog-dark);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* ---------- the stage — below the horizon, in both appearances ---------- */
|
||||
/* The footer and the home sea. The ground is the deep water; raised and
|
||||
recessed plates step up and down from it. Legacy aliases are repeated
|
||||
here because a custom property that references another is computed where
|
||||
it is declared. */
|
||||
/* The footer and the home install band. Legacy aliases are repeated here
|
||||
because a custom property that references another is computed where it is
|
||||
declared. */
|
||||
.stage,
|
||||
.site-footer {
|
||||
--bg: var(--ocean-floor);
|
||||
--surface: var(--ocean-abyss);
|
||||
--panel: var(--ocean-raised);
|
||||
--text: var(--kit-foam);
|
||||
--muted: var(--kit-mist);
|
||||
--line: var(--ocean-line);
|
||||
--line-strong: var(--ocean-line-strong);
|
||||
--accent: var(--kit-bioluminescence);
|
||||
--accent-hover: rgb(143 189 255 / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--kit-abyss);
|
||||
--hover: var(--ocean-hover);
|
||||
--selected: var(--ocean-selected);
|
||||
--selection: rgb(143 189 255 / var(--gpui-selection-opacity));
|
||||
--ring: var(--kit-bioluminescence);
|
||||
--live: var(--kit-kelp);
|
||||
--success: var(--kit-kelp);
|
||||
--attention: var(--kit-lantern);
|
||||
--danger: var(--kit-coral);
|
||||
--mark-ink: var(--kit-foam);
|
||||
--ombre-ink: linear-gradient(135deg, var(--kit-bioluminescence), var(--brand-light));
|
||||
--diff-add: rgb(158 203 189 / 0.16);
|
||||
--diff-del: rgb(238 165 157 / 0.12);
|
||||
--bg: var(--gpui-dark-ocean-depth);
|
||||
--surface: var(--gpui-dark-sidebar);
|
||||
--panel: var(--gpui-dark-panel);
|
||||
--text: var(--gpui-dark-text);
|
||||
--muted: var(--gpui-dark-text-muted);
|
||||
--line: var(--gpui-dark-border);
|
||||
--line-strong: var(--gpui-dark-border-strong);
|
||||
--accent: var(--gpui-dark-accent);
|
||||
--accent-hover: rgb(var(--gpui-dark-accent-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--gpui-dark-on-primary);
|
||||
--brand-fill: var(--gpui-dark-primary);
|
||||
--brand-fill-hover: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-brand: var(--gpui-dark-on-primary);
|
||||
--hover: var(--gpui-dark-hover);
|
||||
--selected: var(--gpui-dark-list-active);
|
||||
--selection: rgb(var(--gpui-dark-accent-rgb) / var(--gpui-selection-opacity));
|
||||
--ring: var(--gpui-dark-accent);
|
||||
--live: var(--gpui-dark-live);
|
||||
--success: var(--gpui-dark-live);
|
||||
--attention: var(--gpui-dark-attention);
|
||||
--danger: var(--gpui-dark-danger);
|
||||
--mark-ink: var(--gpui-dark-text);
|
||||
--ombre-ink: var(--ombre);
|
||||
--diff-add: var(--gpui-dark-diff-add);
|
||||
--diff-del: var(--gpui-dark-diff-remove);
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 20%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 45%, transparent);
|
||||
--glow-focus: 0 0 14px 2px rgb(143 189 255 / 0.35);
|
||||
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.4);
|
||||
--glow-focus: 0 0 0 4px rgb(var(--gpui-dark-accent-rgb) / 0.3);
|
||||
--shadow-popover: var(--gpui-elevation-popover-dark);
|
||||
|
||||
--paper: var(--bg);
|
||||
--paper-deep: var(--surface);
|
||||
|
||||
+76
-17
@@ -251,18 +251,20 @@
|
||||
--gpui-dark-text-rgb: 239 238 235;
|
||||
--gpui-dark-panel: #2a2b2e;
|
||||
--gpui-dark-panel-rgb: 42 43 46;
|
||||
--gpui-dark-text-muted: #c2c1bd;
|
||||
--gpui-dark-text-muted-rgb: 194 193 189;
|
||||
--gpui-dark-text-muted: #b8b8b4;
|
||||
--gpui-dark-text-muted-rgb: 184 184 180;
|
||||
--gpui-dark-border: #3b3c3f;
|
||||
--gpui-dark-border-rgb: 59 60 63;
|
||||
--gpui-dark-sidebar: #191a1c;
|
||||
--gpui-dark-sidebar-rgb: 25 26 28;
|
||||
--gpui-dark-primary: #90b9ff;
|
||||
--gpui-dark-primary-rgb: 144 185 255;
|
||||
--gpui-dark-on-primary: #15243e;
|
||||
--gpui-dark-on-primary-rgb: 21 36 62;
|
||||
--gpui-dark-accent: #303134;
|
||||
--gpui-dark-accent-rgb: 48 49 52;
|
||||
--gpui-dark-primary: #e6e4df;
|
||||
--gpui-dark-primary-rgb: 230 228 223;
|
||||
--gpui-dark-on-primary: #202123;
|
||||
--gpui-dark-on-primary-rgb: 32 33 35;
|
||||
--gpui-dark-accent: #a9c0de;
|
||||
--gpui-dark-accent-rgb: 169 192 222;
|
||||
--gpui-dark-hover: #303134;
|
||||
--gpui-dark-hover-rgb: 48 49 52;
|
||||
--gpui-dark-list-active: #37393d;
|
||||
--gpui-dark-list-active-rgb: 55 57 61;
|
||||
--gpui-dark-attention: #e8b077;
|
||||
@@ -273,24 +275,32 @@
|
||||
--gpui-dark-danger-rgb: 227 154 144;
|
||||
--gpui-dark-border-strong: #828386;
|
||||
--gpui-dark-border-strong-rgb: 130 131 134;
|
||||
--gpui-dark-ocean-depth: #202123;
|
||||
--gpui-dark-ocean-depth-rgb: 32 33 35;
|
||||
--gpui-dark-diff-add: #22392f;
|
||||
--gpui-dark-diff-add-rgb: 34 57 47;
|
||||
--gpui-dark-diff-remove: #45302f;
|
||||
--gpui-dark-diff-remove-rgb: 69 48 47;
|
||||
--gpui-light-bg: #faf8f5;
|
||||
--gpui-light-bg-rgb: 250 248 245;
|
||||
--gpui-light-text: #28292b;
|
||||
--gpui-light-text-rgb: 40 41 43;
|
||||
--gpui-light-panel: #ffffff;
|
||||
--gpui-light-panel-rgb: 255 255 255;
|
||||
--gpui-light-text-muted: #535451;
|
||||
--gpui-light-text-muted-rgb: 83 84 81;
|
||||
--gpui-light-text-muted: #4f504d;
|
||||
--gpui-light-text-muted-rgb: 79 80 77;
|
||||
--gpui-light-border: #d9d5cf;
|
||||
--gpui-light-border-rgb: 217 213 207;
|
||||
--gpui-light-sidebar: #f0ede8;
|
||||
--gpui-light-sidebar-rgb: 240 237 232;
|
||||
--gpui-light-primary: #245bc7;
|
||||
--gpui-light-primary-rgb: 36 91 199;
|
||||
--gpui-light-on-primary: #fbf5ee;
|
||||
--gpui-light-on-primary-rgb: 251 245 238;
|
||||
--gpui-light-accent: #e8e5e0;
|
||||
--gpui-light-accent-rgb: 232 229 224;
|
||||
--gpui-light-primary: #2e2f31;
|
||||
--gpui-light-primary-rgb: 46 47 49;
|
||||
--gpui-light-on-primary: #faf8f5;
|
||||
--gpui-light-on-primary-rgb: 250 248 245;
|
||||
--gpui-light-accent: #425c82;
|
||||
--gpui-light-accent-rgb: 66 92 130;
|
||||
--gpui-light-hover: #e8e5e0;
|
||||
--gpui-light-hover-rgb: 232 229 224;
|
||||
--gpui-light-list-active: #dfdcd6;
|
||||
--gpui-light-list-active-rgb: 223 220 214;
|
||||
--gpui-light-attention: #86520d;
|
||||
@@ -301,8 +311,14 @@
|
||||
--gpui-light-danger-rgb: 158 63 54;
|
||||
--gpui-light-border-strong: #807c76;
|
||||
--gpui-light-border-strong-rgb: 128 124 118;
|
||||
--gpui-light-ocean-depth: #faf8f5;
|
||||
--gpui-light-ocean-depth-rgb: 250 248 245;
|
||||
--gpui-light-diff-add: #d2eadd;
|
||||
--gpui-light-diff-add-rgb: 210 234 221;
|
||||
--gpui-light-diff-remove: #f8e1dd;
|
||||
--gpui-light-diff-remove-rgb: 248 225 221;
|
||||
}
|
||||
/* GPUI design 1.1.2; sha256 433b18a41c526da0b5a1bbaa461c37feac987c7688fbe7de660acfbbdafa9bd5. */
|
||||
/* GPUI design 2.3.0; sha256 9e74fb8f8fbfebb6b90a9740b324c56405277886e61273e2a0cb682797a8a6f9. */
|
||||
:root {
|
||||
--gpui-radius-control: 6px;
|
||||
--gpui-radius-panel: 10px;
|
||||
@@ -317,6 +333,49 @@
|
||||
--gpui-spacing-page: 32px;
|
||||
--gpui-selection-opacity: 0.28;
|
||||
--gpui-primary-hover-opacity: 0.9;
|
||||
--gpui-type-caption-size: 0.75rem;
|
||||
--gpui-type-caption-line: 1.333;
|
||||
--gpui-type-caption-weight: 400;
|
||||
--gpui-type-label-size: 0.8125rem;
|
||||
--gpui-type-label-line: 1.385;
|
||||
--gpui-type-label-weight: 400;
|
||||
--gpui-type-body-size: 0.875rem;
|
||||
--gpui-type-body-line: 1.571;
|
||||
--gpui-type-body-weight: 400;
|
||||
--gpui-type-prose-size: 1rem;
|
||||
--gpui-type-prose-line: 1.625;
|
||||
--gpui-type-prose-weight: 400;
|
||||
--gpui-type-heading-size: 1.0625rem;
|
||||
--gpui-type-heading-line: 1.412;
|
||||
--gpui-type-heading-weight: 550;
|
||||
--gpui-type-title-size: 1.375rem;
|
||||
--gpui-type-title-line: 1.273;
|
||||
--gpui-type-title-weight: 550;
|
||||
--gpui-type-display-size: 1.75rem;
|
||||
--gpui-type-display-line: 1.214;
|
||||
--gpui-type-display-weight: 550;
|
||||
--gpui-type-code-size: 0.8125rem;
|
||||
--gpui-type-code-line: 1.538;
|
||||
--gpui-type-code-weight: 400;
|
||||
--gpui-motion-duration-state: 120ms;
|
||||
--gpui-motion-duration-arrive: 180ms;
|
||||
--gpui-motion-duration-exit: 120ms;
|
||||
--gpui-motion-duration-panel: 340ms;
|
||||
--gpui-motion-duration-linger: 1200ms;
|
||||
--gpui-motion-loop-beat: 1200ms;
|
||||
--gpui-motion-loop-breathe: 2400ms;
|
||||
--gpui-motion-arrive-rise: 4px;
|
||||
--gpui-easing-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--gpui-easing-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--gpui-easing-panel: cubic-bezier(0.2, 0.1, 0.16, 1);
|
||||
--gpui-elevation-popover-light: 0 4px 8px rgb(0 0 0 / 0.16);
|
||||
--gpui-elevation-popover-dark: 0 4px 8px rgb(0 0 0 / 0.4);
|
||||
--gpui-elevation-dialog-light: 0 12px 32px rgb(0 0 0 / 0.2);
|
||||
--gpui-elevation-dialog-dark: 0 12px 32px rgb(0 0 0 / 0.5);
|
||||
--gpui-breakpoint-compact: 720px;
|
||||
--gpui-breakpoint-medium: 1024px;
|
||||
--gpui-breakpoint-wide: 1440px;
|
||||
--gpui-ombre: linear-gradient(135deg, #1e8fd8, #0b48bb);
|
||||
--gpui-font-fallbacks: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC";
|
||||
--gpui-mono-size: 0.8125rem;
|
||||
}
|
||||
@@ -12,11 +12,11 @@
|
||||
.pair label { margin-block-end: .7rem; }
|
||||
.aside { min-width: 0; }
|
||||
.print { display: flex; flex-direction: column; align-items: center; gap: .8rem; padding: 2rem 1.5rem; background: var(--ocean-floor); color: white; border: 1px solid var(--ocean-line); border-radius: var(--radius-sheet); text-align: center; }
|
||||
.caption { font-size: var(--text-ui); color: var(--kit-mist); }
|
||||
.caption { font-size: var(--text-ui); color: var(--ocean-line-strong); }
|
||||
.print img { filter: brightness(0) invert(1); }
|
||||
.printPhrase { margin: .5rem 0; font-size: clamp(1.35rem, 2.8vw, 2rem); font-weight: 650; line-height: 1.4; overflow-wrap: anywhere; }
|
||||
.brand { font-weight: 600; }
|
||||
.site { font-size: var(--text-caption); color: var(--kit-mist); }
|
||||
.site { font-size: var(--text-caption); color: var(--ocean-line-strong); }
|
||||
.cost { margin-block-start: 2rem; }
|
||||
.cost h2, .draft h2 { font-size: var(--text-title); margin-block-end: .5rem; }
|
||||
.cost p, .draft p { color: var(--muted); line-height: 1.6; }
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"sha256": "2653364d1b79ae769390bbd050b2554474128316d73e9a0c7ad931fe47adcd9e",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Ask** posture it applies file\nedits inside the workspace immediately (and shows you the diff), but asks before\nrunning shell commands, so commit or stash anything you care about first. It\nworks with many model providers. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.2 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. Make sure ~/.local/bin is on your PATH, now and in future terminals.\necho 'export PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.bashrc # zsh: use ~/.zshrc\nexport PATH=\"$HOME/.local/bin:$PATH\"\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have no key\n> configured. v0.10.0 doesn't warn you in that case. Press **F3**. If DeepSeek\n> shows `missing key`, press Enter, paste the key, then pick a model and\n> confirm.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recommended installer](#2-recommended-installer-curl--sh)\n3. [Install: manual download from GitHub Releases](#3-manual-download-from-github-releases)\n4. [Install: npm](#4-npm)\n5. [Install: Cargo / build from source](#5-cargo-and-building-from-source)\n6. [Install: Homebrew (Linux) and Nix](#6-homebrew-on-linux-and-nix)\n7. [Updating and rolling back](#Line truncated
|
||||
"sha256": "f96e6d85df956beff9703c9c9c8866ca9c2123e05baea20b23edf3522eb13383",
|
||||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Ask** posture it applies file\nedits inside the workspace immediately (and shows you the diff), but asks before\nrunning shell commands, so commit or stash anything you care about first. It\nworks with many model providers. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.2 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. Make sure ~/.local/bin is on your PATH, now and in future terminals.\necho 'export PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.bashrc # zsh: use ~/.zshrc\nexport PATH=\"$HOME/.local/bin:$PATH\"\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have no key\n> configured. v0.10.0 doesn't warn you in that case. Press **F3**. If DeepSeek\n> shows `missing key`, press Enter, paste the key, then pick a model and\n> confirm.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recommended installer](#2-recommended-installer-curl--sh)\n3. [Install: manual download from GitHub Releases](#3-manual-download-from-github-releases)\n4. [Install: npm](#4-npm)\n5. [Install: Cargo / build from source](#5-cargo-and-building-from-source)\n6. [Install: Homebrew (Linux) and Nix](#6-homebrew-on-linux-and-nix)\n7. [Updating and rolling back](#Line truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -21,8 +21,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/KEYBINDINGS.md",
|
||||
"sha256": "fa8d8f7bfac4231f66e064202bf7cf335177a4a6a2b21b725630cbba22f94793",
|
||||
"content": "# Keybindings\n\n> 阅读简体中文版:[zh_hans/KEYBINDINGS.md](zh_hans/KEYBINDINGS.md)\n\nThis is the source-of-truth catalog of every keyboard shortcut the TUI recognizes. Bindings are grouped by **context** — the focus or modal state they fire in. A binding listed under \"Composer\" only takes effect when the composer is focused; one under \"Transcript\" only when the transcript has focus; and so on.\n\nGlobal key chords are not yet user-configurable — tracked for a future release (#436, #437). Hotbar slot actions are configurable with `[[hotbar]]` and `/hotbar`; the Hotbar activation chord remains `Alt-1` through `Alt-8`.\n\n## Global (any context)\n\n| Chord | Action |\n|----------------------|---------------------------------------------------------------|\n| `/help`, `F1` or `Ctrl-/` | Toggle the help overlay. `/help` is what the chrome advertises: it is the one route every terminal delivers, where `F1` is eaten by tmux and some emulators and `Ctrl-/` encoding varies |\n| `F2` | Toggle the typed Settings editor |\n| `F3` | Open the provider/model picker (same as `/provider`) |\n| `Ctrl-K` | Open the command palette (slash-command finder) |\n| `Ctrl-C` | Cancel current turn / dismiss modal / clear a non-empty draft (recoverable — see `Ctrl-Z`) / arm-then-confirm quit |\n| `Ctrl-B` | Release the current shell wait so the turn can continue; foreground commands move into `/jobs`, and background tasks keep running. Use `/jobs` or `Bash` with `action: \"wait\"` to inspect output |\n| `Ctrl-D` | Quit (only when the composer is empty) |\n| `Tab` | When the composer is empty, cycle TUI mode: Plan → Work → Operate → Plan |\n| `Shift+Tab` | Cycle permission posture: Ask → Auto-Review → Full Access. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open) |\n| `Ctrl-T` | Cycle reasoning effort for the active model. Walks the same ladder as `/model` and `/effort` (catalog or documented route dialect). Always-thinking models omit `off`; Grok 4.6 includes `xhigh`. |\n| `Ctrl-Shift-T` | Toggle live transcript overlay (sticky-tail auto-scroll) |\n| `Ctrl-R` | Open the resume-session picker |\n| `Ctrl-L` | Compact the conversation context (status line shows progress; no-op while a compaction is already running) |\n| `Ctrl-O` | Open the reasoning detail for the selected or current turn, regardless of composer contents |\n| `Ctrl-Alt-O` | Open the whole-turn Turn Inspector, regardless of composer contents |\n| `Alt-V` / `Option-V` (macOS) | Open the details pager for the selected tool/sub-agent card; with nothing selected, the newest visible error, else the first visible card with details, else the most recent one. Click a card first to keep Alt-V on it while newer cards arrive. Terminals that emit the legacy Option-V glyph are also handled |\n| `Ctrl-Shift-E` / `Cmd-Shift-E` | Toggle the file tree in the workbar |\n| `Ctrl-]` or `Ctrl-Tab` | Cycle the pet view while the pet main view is showing; otherwise cycle the work dock: TODO → Agents → Jobs → Background. `Ctrl-]` is portable; `Ctrl-Tab` requires the kitty keyboard protocol. Terminal.app uses it to switch terminal tabs |\n| `Ctrl-Shift-Tab` | Cycle the pet view or work dock backwards where the terminal sends the kitty keyboard chord. Terminal.app uses it to switch terminal tabs |\n| `Alt-G` / `Alt-Shift-G` | Scroll transcript to top / bottom when the composer is empty |\n| `Alt-1`-`Alt-8` | Dispatch Hotbar slots 1-8 when no modal or inline picker is open |\n| `Alt-!` / `Alt-@` / `Alt-#` / `Alt-$` | Select the workbar panel: Tasks / Agents / Context / Files |\n| `Ctrl-Alt-0` | Toggle the workbar off / back to the bottom placement |\n| `Alt-L` | Open the pager for the last message (composer empty) |\n| `Alt-P` / `Alt-A` / `Alt-Y` | Jump to Plan / Work, or request Full Access (`Alt-Y` is the legacy permission channel — Work + Full Access — not a separate mode; it honors a locked approval policy) |\n| `Ctrl-X` (Activity workbar) | Cancel all running background shell jobs |\n| `Esc` | Close topmost modal · cancel slash menu · dismiss toast. During a compact that is serving an in-flight turn, Esc stops the turn (the compact is collateral). |\n\n## Composer\n\nEditing the message you're about to send.\n\n| Chord | Action |\n|-----------------------------|---------------------------------------------------------|\n| `Enter` Line truncated
|
||||
"sha256": "72a081c68ffba992e80c288141b1b82f5e5a767145f60cdabc71f2f92fba48e9",
|
||||
"content": "# Keybindings\n\n> 阅读简体中文版:[zh_hans/KEYBINDINGS.md](zh_hans/KEYBINDINGS.md)\n\nThis is the source-of-truth catalog of every keyboard shortcut the TUI recognizes. Bindings are grouped by **context** — the focus or modal state they fire in. A binding listed under \"Composer\" only takes effect when the composer is focused; one under \"Transcript\" only when the transcript has focus; and so on.\n\nGlobal key chords are not yet user-configurable — tracked for a future release (#436, #437). Hotbar slot actions are configurable with `[[hotbar]]` and `/hotbar`; the Hotbar activation chord remains `Alt-1` through `Alt-8`.\n\n## Global (any context)\n\n| Chord | Action |\n|----------------------|---------------------------------------------------------------|\n| `/help`, `F1` or `Ctrl-/` | Toggle the help overlay. `/help` is what the chrome advertises: it is the one route every terminal delivers, where `F1` is eaten by tmux and some emulators and `Ctrl-/` encoding varies |\n| `F2` | Toggle the typed Settings editor |\n| `F3` | Open the provider/model picker (same as `/provider`) |\n| `Ctrl-K` | Open the command palette (slash-command finder) |\n| `Ctrl-C` | Cancel current turn / dismiss modal / clear a non-empty draft (recoverable — see `Ctrl-Z`) / arm-then-confirm quit |\n| `Ctrl-B` | Release the current shell wait so the turn can continue; foreground commands move into `/jobs`, and background tasks keep running. Use `/jobs` or `Bash` with `action: \"wait\"` to inspect output |\n| `Ctrl-D` | Quit (only when the composer is empty) |\n| `Tab` | When the composer is empty, cycle TUI mode: Plan → Work → Operate → Plan. Works while a turn is running: the change applies at the agent's next step in the same turn, and the status line says so until it does |\n| `Shift+Tab` | Cycle permission posture: Ask → Auto-Review → Full Access. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open). During a turn the change applies at the agent's next step; a call you already approved runs only if the new posture is equal or broader. Model, thinking level and provider stay locked until the turn ends |\n| `Ctrl-T` | Cycle reasoning effort for the active model. Walks the same ladder as `/model` and `/effort` (catalog or documented route dialect). Always-thinking models omit `off`; Grok 4.6 includes `xhigh`. |\n| `Ctrl-Shift-T` | Toggle live transcript overlay (sticky-tail auto-scroll) |\n| `Ctrl-R` | Open the resume-session picker |\n| `Ctrl-L` | Compact the conversation context (status line shows progress; no-op while a compaction is already running) |\n| `Ctrl-O` | Open the reasoning detail for the selected or current turn, regardless of composer contents |\n| `Ctrl-Alt-O` | Open the whole-turn Turn Inspector, regardless of composer contents |\n| `Alt-V` / `Option-V` (macOS) | Open the details pager for the selected tool/sub-agent card; with nothing selected, the newest visible error, else the first visible card with details, else the most recent one. Click a card first to keep Alt-V on it while newer cards arrive. Terminals that emit the legacy Option-V glyph are also handled |\n| `Ctrl-Shift-E` / `Cmd-Shift-E` | Toggle the file tree in the workbar |\n| `Ctrl-]` or `Ctrl-Tab` | Cycle the pet view while the pet main view is showing; otherwise cycle the work dock: TODO → Agents → Jobs → Background. `Ctrl-]` is portable; `Ctrl-Tab` requires the kitty keyboard protocol. Terminal.app uses it to switch terminal tabs |\n| `Ctrl-Shift-Tab` | Cycle the pet view or work dock backwards where the terminal sends the kitty keyboard chord. Terminal.app uses it to switch terminal tabs |\n| `Alt-G` / `Alt-Shift-G` | Scroll transcript to top / bottom when the composer is empty |\n| `Alt-1`-`Alt-8` | Dispatch Hotbar slots 1-8 when no modal or inline picker is open |\n| `Alt-!` / `Alt-@` / `Alt-#` / `Alt-$` | Select the workbar panel: Tasks / Agents / Context / Files |\n| `Ctrl-Alt-0` | Toggle the workbar off / back to the bottom placement |\n| `Alt-L` | Open the pager for the last message (composer empty) |\n| `Alt-P` / `Alt-A` / `Alt-Y` | Jump to Plan / Work, or request Full Access (`Alt-Y` is the legacy permission channel — Work + Full Access — not a separate mode; it honors a locked approval policy) |\n| `Ctrl-X` (Activity workbar) | Cancel all running background shell jobs |\n| `Esc` | Close topmost modal · cancel slash menu · dismiss toast. During a compact Line truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -32,8 +32,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "3e076746af15d2c2f25bd60d86648c95ca17fc98ec0d9b1bd4d3a025958dd5f3",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
|
||||
"sha256": "c36db36c85ad3be7d11a01dbdf0c74a5641844a62169431db5988a127a34cd69",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/CODEWHALE_AGENT.md",
|
||||
@@ -48,8 +48,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/PROVIDERS.md",
|
||||
"sha256": "a1b48aeee2b2f2aadc60b1c1ee8646cef8733c5f9a4c1f8087e958fa53123ffd",
|
||||
"content": "# Provider Registry\n\n> 阅读简体中文版:[zh_hans/PROVIDERS.md](zh_hans/PROVIDERS.md)\n\nSpeech recognition, synthesis, translation and video have separate execution\npaths. See [media models](MEDIA_MODELS.md) for current support and reviewed\nupstream candidates; a chat catalogue row alone does not add a media backend.\n\nThis registry describes provider behavior that is wired into the current\nCodewhale codebase. It is intentionally conservative: shipped entries are\nlimited to provider IDs, config keys, auth paths, base URLs, model resolution,\nand capability metadata that the code already knows about.\n\nThe `/provider` and `/model` pickers recommend OrcaRouter as a partner option.\nClick the recommendation or press F7 to view the Codewhale × OrcaRouter offer:\na **30% top-up match for DeepSeek V4.1 Flash**. The pager includes the\n[redemption link](https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67)\nand can copy its text. Codewhale earns a referral fee from OrcaRouter.\nCurrent eligibility and terms are owned by the linked offer page. Viewing it\npreserves the current provider, model and credentials; redemption happens on\nOrcaRouter's site.\n\nDeepSeek remains the default provider, but every entry in `ProviderKind::ALL`\nis a first-class selectable provider route. `ALL` is the catalog/picker\nsurface — one identity per vendor. Dual-wire dialect kinds (`*Anthropic`, e.g.\n`deepseek-anthropic`) and the Model Studio plan variants stay on the enum for\nserde and `provider_for_kind` but are deliberately **not** catalog rows:\na plan is `mode`/`base_url` and a dialect is `wire = openai|anthropic` on the\nprimary provider config (`crates/config/src/provider_kind.rs:221-226`). Hosted\nroutes, generic OpenAI-compatible endpoints, the OpenAI Codex/ChatGPT route,\nnative Anthropic, and local runtimes all run the same terminal harness against\nthe selected provider/model/base URL.\n\nA host reached over plain Chat Completions is an ordinary named provider,\nnot a `ProviderKind`: enum variants are reserved for distinct *wires*\n(Anthropic Messages, Codex Responses, Google thought signatures). Any such\nhost is a `[providers.<name>]` table with a base URL, a model, and a key env\n(`docs/CONFIGURATION.md`); `/provider` and `/setup` keep a \"paste a Base URL\nand a key\" path for exactly this. Offerings come from live `GET /v1/models`\nplus the Codewhale catalog rather than a compiled roster (#5350, #6289).\n\nKnown-good hosts. These ship as bundled descriptor rows in\n`crates/config/assets/provider_descriptors.json` (compiled in by\n`crates/config/src/descriptors.rs`): each row says how to reach the host — wire,\nbase URL, key env, aliases — while model ids stay live from `GET /v1/models`\nand the Codewhale catalog; the example model is only a bootstrap hint. Verify\nagainst the vendor's own docs before trusting any value here:\n\n| Host | Base URL | Example models | API key env |\n| --- | --- | --- | --- |\n| SenseNova | `https://token.sensenova.cn/v1` | `deepseek-v4-flash` | `SENSENOVA_API_KEY` |\n| Baseten | `https://inference.baseten.co/v1` | `deepseek-ai/DeepSeek-V4-Pro` | `BASETEN_API_KEY` |\n| Groq | `https://api.groq.com/openai/v1` | `llama-3.3-70b-versatile` | `GROQ_API_KEY` |\n| Cerebras | `https://api.cerebras.ai/v1` | `llama-3.3-70b` | `CEREBRAS_API_KEY` |\n| Command Code | `https://api.commandcode.ai/provider/v1` | `deepseek/deepseek-v4-flash` | `COMMAND_CODE_API_KEY` |\n| Alibaba Model Studio (DashScope) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `qwen3.8-flash` | `DASHSCOPE_API_KEY` |\n| AICraft | `https://aicraftapi.com/v1` | `claude-4.6-sonnet`; DeepSeek / Claude / Gemini / Qwen / GLM / MiniMax / Doubao families | `AICRAFT_API_KEY` |\n| Tsubasa | `https://api.tsubasa.sh/v1` | `tsubasa-pro`, `tsubasa-fast` (32,768-token context) | `TSUBASA_API_KEY` |\n| Cheaper Inference | `https://api.cheaperinference.com/v1` | `gpt-5.4-mini`, `claude-sonnet-5`, `gemini-3.1-pro` | `CHEAPER_INFERENCE_API_KEY` |\n| Yolo-Auto | `https://yolo-auto.com/v1` | `qwen3.8-flash`; `yolo` / `yolo-small` | `YOLO_AUTO_API_KEY` |\n| Standard Compute | `https://api.stdcmpt.com/v1` | `standardcompute` (router); `deepseek/deepseek-v4-pro`, `anthropic/claude-sonnet-5.5` | `STANDARDCOMPUTE_API_KEY` |\n\nAICraft's roster spans DeepSeek, Anthropic Claude, Google Gemini, Qwen, GLM,\nMiniMax and Doubao ids on its OpenAI-compatible endpoint. The authority is\n`GET https://aicraftapi.com/v1/models` with your key — pick a model from that\nlist, not from this table.\nTsubasa implements only `GET /v1/models` and Chat Completions. Its two public\nmodel ids share a 32,768-token context, smaller than the 128,000 tokens Codewhale assumes\nfor an unknown model, so set it on the route after saving:\n`codewhale config set providers.tsubasa.context_window 32768`.\n\nCheaper Inference is an OpenAI-compatible gateway with one key for models from\nseveral labs. Model ids are bare, sucLine truncated
|
||||
"sha256": "39efe1ac88aaf5752a54c9267ffd9f26066bd4b46984713602d8bd14adb46508",
|
||||
"content": "# Provider Registry\n\n> 阅读简体中文版:[zh_hans/PROVIDERS.md](zh_hans/PROVIDERS.md)\n\nSpeech recognition, synthesis, translation and video have separate execution\npaths. See [media models](MEDIA_MODELS.md) for current support and reviewed\nupstream candidates; a chat catalogue row alone does not add a media backend.\n\nThis registry describes provider behavior that is wired into the current\nCodewhale codebase. It is intentionally conservative: shipped entries are\nlimited to provider IDs, config keys, auth paths, base URLs, model resolution,\nand capability metadata that the code already knows about.\n\nThe `/provider` and `/model` pickers recommend OrcaRouter as a partner option.\nClick the recommendation or press F7 to view the Codewhale × OrcaRouter offer:\na **30% top-up match for DeepSeek V4.1 Flash**. The pager includes the\n[redemption link](https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67)\nand can copy its text. Codewhale earns a referral fee from OrcaRouter.\nCurrent eligibility and terms are owned by the linked offer page. Viewing it\npreserves the current provider, model and credentials; redemption happens on\nOrcaRouter's site.\n\nDeepSeek remains the default provider, but every entry in `ProviderKind::ALL`\nis a first-class selectable provider route. `ALL` is the catalog/picker\nsurface — one identity per vendor. Dual-wire dialect kinds (`*Anthropic`, e.g.\n`deepseek-anthropic`) and the Model Studio plan variants stay on the enum for\nserde and `provider_for_kind` but are deliberately **not** catalog rows:\na plan is `mode`/`base_url` and a dialect is `wire = openai|anthropic` on the\nprimary provider config (`crates/config/src/provider_kind.rs:221-226`). Hosted\nroutes, generic OpenAI-compatible endpoints, the OpenAI Codex/ChatGPT route,\nnative Anthropic, and local runtimes all run the same terminal harness against\nthe selected provider/model/base URL.\n\nA host reached over plain Chat Completions is an ordinary named provider,\nnot a `ProviderKind`: enum variants are reserved for distinct *wires*\n(Anthropic Messages, Codex Responses, Google thought signatures). Any such\nhost is a `[providers.<name>]` table with a base URL, a model, and a key env\n(`docs/CONFIGURATION.md`); `/provider` and `/setup` keep a \"paste a Base URL\nand a key\" path for exactly this. Offerings come from live `GET /v1/models`\nplus the Codewhale catalog rather than a compiled roster (#5350, #6289).\n\nKnown-good hosts. These ship as bundled descriptor rows in\n`crates/config/assets/provider_descriptors.json` (compiled in by\n`crates/config/src/descriptors.rs`): each row says how to reach the host — wire,\nbase URL, key env, aliases — while model ids stay live from `GET /v1/models`\nand the Codewhale catalog; the example model is only a bootstrap hint. Verify\nagainst the vendor's own docs before trusting any value here:\n\n| Host | Base URL | Example models | API key env |\n| --- | --- | --- | --- |\n| SenseNova | `https://token.sensenova.cn/v1` | `deepseek-v4-flash` | `SENSENOVA_API_KEY` |\n| Baseten | `https://inference.baseten.co/v1` | `deepseek-ai/DeepSeek-V4-Pro` | `BASETEN_API_KEY` |\n| Groq | `https://api.groq.com/openai/v1` | `llama-3.3-70b-versatile` | `GROQ_API_KEY` |\n| Cerebras | `https://api.cerebras.ai/v1` | `llama-3.3-70b` | `CEREBRAS_API_KEY` |\n| Command Code | `https://api.commandcode.ai/provider/v1` | `deepseek/deepseek-v4-flash` | `COMMAND_CODE_API_KEY` |\n| Alibaba Model Studio (DashScope) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `qwen3.8-flash` | `DASHSCOPE_API_KEY` |\n| AICraft | `https://aicraftapi.com/v1` | `claude-4.6-sonnet`; DeepSeek / Claude / Gemini / Qwen / GLM / MiniMax / Doubao families | `AICRAFT_API_KEY` |\n| Tsubasa | `https://api.tsubasa.sh/v1` | `tsubasa-pro`, `tsubasa-fast` (32,768-token context) | `TSUBASA_API_KEY` |\n| Cheaper Inference | `https://api.cheaperinference.com/v1` | `gpt-5.4-mini`, `claude-sonnet-5`, `gemini-3.1-pro` | `CHEAPER_INFERENCE_API_KEY` |\n| Yolo-Auto | `https://yolo-auto.com/v1` | `qwen3.8-flash`; `yolo` / `yolo-small` | `YOLO_AUTO_API_KEY` |\n| Standard Compute | `https://api.stdcmpt.com/v1` | `standardcompute` (router); `deepseek/deepseek-v4-pro`, `anthropic/claude-sonnet-5.5` | `STANDARDCOMPUTE_API_KEY` |\n\nAICraft's roster spans DeepSeek, Anthropic Claude, Google Gemini, Qwen, GLM,\nMiniMax and Doubao ids on its OpenAI-compatible endpoint. The authority is\n`GET https://aicraftapi.com/v1/models` with your key — pick a model from that\nlist, not from this table.\nTsubasa implements only `GET /v1/models` and Chat Completions. Its two public\nmodel ids share a 32,768-token context, smaller than the 128,000 tokens Codewhale assumes\nfor an unknown model, so set it on the route after saving:\n`codewhale config set providers.tsubasa.context_window 32768`.\n\nCheaper Inference is an OpenAI-compatible gateway with one key for models from\nseveral labs. Model ids are bare, sucLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/MODEL_LAB.md",
|
||||
@@ -64,13 +64,13 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "3e076746af15d2c2f25bd60d86648c95ca17fc98ec0d9b1bd4d3a025958dd5f3",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
|
||||
"sha256": "c36db36c85ad3be7d11a01dbdf0c74a5641844a62169431db5988a127a34cd69",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/LEGACY_PATHS.md",
|
||||
"sha256": "89f7a556479d57911d9589af582c13a0c7494418d9d47f430d1bfa6c74ad3333",
|
||||
"content": "# Legacy `.deepseek/` compatibility paths — audit & migration status (#3068)\n\n> 阅读简体中文版:[zh_hans/LEGACY_PATHS.md](zh_hans/LEGACY_PATHS.md)。\n\n## v0.9.10 cleanse ledger\n\nThis is the release-window decision record for residue that can otherwise look\ninterchangeable in a text search. A compatibility entry must name the contract\nthat still requires it; new production code must use a current path.\n\n| Class | Surface | Decision / contract |\n|---|---|---|\n| DELETE | Retired underwater build-version test override | Removed in `1cc4d1d1b`; the owner test was already deleted and the production-only build version remains authoritative. |\n| MIGRATE | Cross-task Agent Mail | `bd998495a` and managed-app `4ff7bc50d` replace guessed event aliases and same-session mailbox conflation with one durable typed runtime protocol. |\n| MIGRATE | TUI `status_message` writes | Active residue; touched flows must move to the typed toast/current-state owner. No new writes are allowed. |\n| COMPATIBILITY | `.deepseek/` state and stored provider aliases | Read fallback for upgraded installations and stored provider identity; writes remain Codewhale-only. Details are below. |\n| COMPATIBILITY | `agent_message` transcript/protocol decoding | Persisted transcripts and tool wire values require decoding; it is not the cross-task Agent Mail transport. |\n| COMPATIBILITY | Managed runtime envelope schema 1 | Previously persisted/enrolled runner replay; current local Codewhale Agent Mail producer is schema 2 and both normalize to the managed envelope. |\n| CURRENT | DeepSeek provider support | A real selectable provider, not product branding; provider-scoped names and configuration remain. |\n| CURRENT | Same-session subagent mailbox | Start/status/peek/message/followup/interrupt/wait/cancel stays scoped to a parent runtime session. |\n| CURRENT | Web locale dictionaries | The application and documentation routes have no page-local `isZh` branches; new copy continues through dictionaries. |\n| CURRENT | Safety, authorization, protocol, persistence, migration, and data-integrity tests | These protect external or durable contracts and are not copy/layout cleanup candidates. |\n\nSnapshot on 2026-08-19: `10,954` Rust `#[test]` attributes under\n`crates/tui`, `523` TUI `status_message` source lines, and zero page-local\n`isZh` files under `apps/web/app` in the paired managed-app candidate. Counts\nare inventory signals, not deletion targets.\n\nCodewhale was renamed from DeepSeek-TUI. To avoid breaking existing installs, the runtime reads\nstate from the new `~/.codewhale/` location but **falls back** to the legacy `~/.deepseek/` location,\nand always **writes** to `~/.codewhale/`. This doc audits each legacy reference and records a\nkeep / deprecate / remove decision so the migration is auditable.\n\n## The canonical resolver (use this for new code)\n\nState-dir resolution is consolidated in `crates/config/src/lib.rs`:\n\n| Symbol | Line | Purpose |\n|---|---|---|\n| `CODEWHALE_APP_DIR` / `LEGACY_APP_DIR` | 5369 | re-exported from `crates/paths` (defined at `crates/paths/src/lib.rs` 13 / 16) |\n| `codewhale_home()` | 5375 | `~/.codewhale` |\n| `legacy_deepseek_home()` | 5392 | `~/.deepseek` (legacy) |\n| `resolve_state_dir(subdir)` | 5434 | **read** path: `~/.codewhale/<subdir>`, falling back to `~/.deepseek/<subdir>` when only the legacy dir exists |\n| `ensure_state_dir(subdir)` | 5458 | **write** path: always creates under `~/.codewhale/<subdir>` |\n\nMigration contract: read-with-fallback, write-to-new. This preserves the v0.8.44 migration for\nusers who still have `~/.deepseek/` while steering all new writes to `~/.codewhale/`.\n\n## Per-path decisions\n\n**Decision for all legacy references below: keep-as-fallback.** Removing the `.deepseek` fallback\nwould strand users who upgraded in place and never re-ran onboarding. Revisit only after a release\nthat actively migrates `~/.deepseek/` → `~/.codewhale/` on first run and a deprecation window.\n\n| Reference | Routed through `resolve_state_dir`? | Decision |\n|---|---|---|\n| `config::resolve_state_dir` / `ensure_state_dir` | n/a (the resolver itself) | keep — canonical |\n| `crates/tui/src/skills/mod.rs` (`~/.deepseek/skills`) | no — hardcoded | keep-as-fallback; route through resolver in a follow-up refactor |\n| `crates/tui/src/prompts.rs` (`LEGACY_HANDOFF_RELATIVE_PATH = \".deepseek/handoff.md\"`) | no — explicit legacy const | keep — explicit legacy handoff fallback |\n| `crates/tui/src/workspace_trust.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/tui/src/session_manager.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/runtime/src/skill_state.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/tui/src/tools/skill.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/tui/src/snapshot/mod.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/runtime/src/woLine truncated
|
||||
"sha256": "614ee8108d5f9abeed798b9d8cf2acf23a38907a311218bd6f849f067e34be2c",
|
||||
"content": "# Legacy `.deepseek/` compatibility paths — audit & migration status (#3068)\n\n> 阅读简体中文版:[zh_hans/LEGACY_PATHS.md](zh_hans/LEGACY_PATHS.md)。\n\n## v0.9.10 cleanse ledger\n\nThis is the release-window decision record for residue that can otherwise look\ninterchangeable in a text search. A compatibility entry must name the contract\nthat still requires it; new production code must use a current path.\n\n| Class | Surface | Decision / contract |\n|---|---|---|\n| DELETE | Retired underwater build-version test override | Removed in `1cc4d1d1b`; the owner test was already deleted and the production-only build version remains authoritative. |\n| MIGRATE | Cross-task Agent Mail | `bd998495a` and managed-app `4ff7bc50d` replace guessed event aliases and same-session mailbox conflation with one durable typed runtime protocol. |\n| MIGRATE | TUI `status_message` writes | Active residue; touched flows must move to the typed toast/current-state owner. No new writes are allowed. |\n| COMPATIBILITY | `.deepseek/` state and stored provider aliases | Read fallback for upgraded installations and stored provider identity; writes remain Codewhale-only. Details are below. |\n| COMPATIBILITY | `agent_message` transcript/protocol decoding | Persisted transcripts and tool wire values require decoding; it is not the cross-task Agent Mail transport. |\n| COMPATIBILITY | Managed runtime envelope schema 1 | Previously persisted/enrolled runner replay; current local Codewhale Agent Mail producer is schema 2 and both normalize to the managed envelope. |\n| CURRENT | DeepSeek provider support | A real selectable provider, not product branding; provider-scoped names and configuration remain. |\n| CURRENT | Same-session subagent mailbox | Start/status/peek/message/followup/interrupt/wait/cancel stays scoped to a parent runtime session. |\n| CURRENT | Web locale dictionaries | The application and documentation routes have no page-local `isZh` branches; new copy continues through dictionaries. |\n| CURRENT | Safety, authorization, protocol, persistence, migration, and data-integrity tests | These protect external or durable contracts and are not copy/layout cleanup candidates. |\n\nSnapshot on 2026-08-19: `10,954` Rust `#[test]` attributes under\n`crates/tui`, `523` TUI `status_message` source lines, and zero page-local\n`isZh` files under `apps/web/app` in the paired managed-app candidate. Counts\nare inventory signals, not deletion targets.\n\nCodewhale was renamed from DeepSeek-TUI. To avoid breaking existing installs, the runtime reads\nstate from the new `~/.codewhale/` location but **falls back** to the legacy `~/.deepseek/` location,\nand always **writes** to `~/.codewhale/`. This doc audits each legacy reference and records a\nkeep / deprecate / remove decision so the migration is auditable.\n\n## The canonical resolver (use this for new code)\n\nState-dir resolution is consolidated in `crates/config/src/lib.rs`:\n\n| Symbol | Line | Purpose |\n|---|---|---|\n| `CODEWHALE_APP_DIR` / `LEGACY_APP_DIR` | 5369 | re-exported from `crates/paths` (defined at `crates/paths/src/lib.rs` 13 / 16) |\n| `codewhale_home()` | 5375 | `~/.codewhale` |\n| `legacy_deepseek_home()` | 5392 | `~/.deepseek` (legacy) |\n| `resolve_state_dir(subdir)` | 5434 | **read** path: `~/.codewhale/<subdir>`, falling back to `~/.deepseek/<subdir>` when only the legacy dir exists |\n| `ensure_state_dir(subdir)` | 5458 | **write** path: always creates under `~/.codewhale/<subdir>` |\n\nMigration contract: read-with-fallback, write-to-new. This preserves the v0.8.44 migration for\nusers who still have `~/.deepseek/` while steering all new writes to `~/.codewhale/`.\n\n## Per-path decisions\n\n**Decision for all legacy references below: keep-as-fallback.** Removing the `.deepseek` fallback\nwould strand users who upgraded in place and never re-ran onboarding. Revisit only after a release\nthat actively migrates `~/.deepseek/` → `~/.codewhale/` on first run and a deprecation window.\n\n| Reference | Routed through `resolve_state_dir`? | Decision |\n|---|---|---|\n| `config::resolve_state_dir` / `ensure_state_dir` | n/a (the resolver itself) | keep — canonical |\n| `crates/tui/src/skills/mod.rs` (`~/.deepseek/skills`) | no — hardcoded | keep-as-fallback; route through resolver in a follow-up refactor |\n| `crates/tui/src/prompts.rs` (`LEGACY_HANDOFF_RELATIVE_PATH = \".deepseek/handoff.md\"`) | no — explicit legacy const | keep — explicit legacy handoff fallback |\n| `crates/tui/src/workspace_trust.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/tui/src/session_manager.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/runtime/src/skill_state.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/tui/src/tools/skill.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/snapshot/src/lib.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/runtime/src/workspLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -80,8 +80,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/MODES.md",
|
||||
"sha256": "06bc6d00b279d7a3d0479b370893a900139e593a6a59db6b0d574e621f86363e",
|
||||
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work/Operate).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not part of\nthe `Tab` cycle.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nPress `Tab` to complete composer menus or cycle through the visible modes\nwhen the composer is empty: **Plan → Work → Operate → Plan**. `Tab` never sends\nor queues composer text; use `Enter` to send or queue it.\nPress `Shift+Tab` to cycle permission posture (Ask → Auto-Review → Full Access).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work`,\n`/mode plan`, or `/mode operate`.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `bash`, `agent`, `workflow`, `todo_write`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate**: Work at full strength, the \"ultra\" mode. Same tools and authority as Work; the difference is how they are used. Each substantive request becomes a goal (`create_goal`), the plan stays visible (`todo_write`), multi-part work runs through `workflow` and parallel `agent` workers, non-trivial changes get an independent reviewer before they are called done, long commands keep running in the background, and recurring or watch-type work is offered as an `automation` (created after approval). It keeps going until the goal is verified, blocked on you, or paused.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work | Operate |\n|:---|:---:|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated | same as Work |\n| `bash` | visible name; execution denied | approval- and policy-gated | same as Work; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same | same |\n| Paid or external-service tools | follows permission posture | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode | same trusted-path/trust policy as Work; fleet profiles never widen it |\n\nOperate changes scheduling emphasis, not authority. It neither adds a\nmode-specific tool denial nor bypasses the active approval, sandbox, shell,\nask-rule, repository-law, or managed-policy boundary. Plan remains the\nmode-specific execution boundary for shell and write-capable tools; that\nauthority difference does not require a different primitive vocabulary.\n\n### Operate loop (one screen)\n\n```text\nUser Line truncated
|
||||
"sha256": "e327b2042c9b782660cb0d6de9f4208364537ef1052a456a45332a61b1ee2468",
|
||||
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work/Operate).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not part of\nthe `Tab` cycle.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nPress `Tab` to complete composer menus or cycle through the visible modes\nwhen the composer is empty: **Plan → Work → Operate → Plan**. `Tab` never sends\nor queues composer text; use `Enter` to send or queue it.\nPress `Shift+Tab` to cycle permission posture (Ask → Auto-Review → Full Access).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work`,\n`/mode plan`, or `/mode operate`.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `bash`, `agent`, `workflow`, `todo_write`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate**: Work at full strength, the \"ultra\" mode. Same tools and authority as Work; the difference is how they are used. Each substantive request becomes a goal (`create_goal`), the plan stays visible (`todo_write`), multi-part work runs through `workflow` and parallel `agent` workers, non-trivial changes get an independent reviewer before they are called done, long commands keep running in the background, and recurring or watch-type work is offered as an `automation` (created after approval). It keeps going until the goal is verified, blocked on you, or paused.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work | Operate |\n|:---|:---:|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated | same as Work |\n| `bash` | visible name; execution denied | approval- and policy-gated | same as Work; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same | same |\n| Paid or external-service tools | follows permission posture | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode | same trusted-path/trust policy as Work; fleet profiles never widen it |\n\nOperate changes scheduling emphasis, not authority. It neither adds a\nmode-specific tool denial nor bypasses the active approval, sandbox, shell,\nask-rule, repository-law, or managed-policy boundary. Plan remains the\nmode-specific execution boundary for shell and write-capable tools; that\nauthority difference does not require a different primitive vocabulary.\n\n### Operate loop (one screen)\n\n```text\nUser Line truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -96,8 +96,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "3e076746af15d2c2f25bd60d86648c95ca17fc98ec0d9b1bd4d3a025958dd5f3",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
|
||||
"sha256": "c36db36c85ad3be7d11a01dbdf0c74a5641844a62169431db5988a127a34cd69",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -187,8 +187,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/TELEMETRY.md",
|
||||
"sha256": "6e69aec8fe595b6d40bd63c71cfaea6cc980ca7b1adaa09b9e4920c50bfe3d53",
|
||||
"content": "# Codewhale product telemetry\n\n> 阅读简体中文版:[zh_hans/TELEMETRY.md](zh_hans/TELEMETRY.md)\n\n**Usage analytics are on by default in the current source.** The first\ninteractive launch gives a localized, nonblocking disclosure naming **Codewhale\nand PostHog**, with a direct route to turn counting off. Notice version `5`\ndescribes this policy; displaying it is not recorded as human acceptance.\nMissing an older acceptance record does not turn a fresh installation off.\nExisting explicit opt-outs remain off across upgrades, and unreadable privacy\nstate fails closed. Environment and CLI kill switches still take precedence.\n\nRead the disclosure with `codewhale config telemetry`. Turn usage off in\n`/settings` or run `codewhale config set telemetry false`. Turning it back on\nin Settings or with `codewhale config set telemetry true` updates the existing\ndurable preference and privacy records for new sessions. The compatibility\ncommand `codewhale config telemetry --accept-notice 5` remains available, but\nis not required for a fresh installation. Collection never includes work content or credentials.\n\nThe PostHog sink is optional and inert until the ingest operator explicitly\nconfigures a project token and approved regional host, and records staging\nproof that the actual egress path does not forward the original client IP.\nSource readiness does not establish a deployment, an activated processor,\nan egress receipt, or a retention setting.\n\nCodewhale does not collect conversations, code, prompts, files, file/repo/branch\nnames, model content, or credentials. It sends no per-turn or per-tool timeline.\nIt does send the closed, aggregate schema below: version and platform classes,\nsession duration/outcome, feature/error counters, and a random install id that\nrotates every 90 days.\n\n**There is now a real endpoint.** An enabled session sends its batches to the\nfirst-party ingest service at `https://telemetry.codewhale.net/v1/telemetry`,\nwhich is the shipped default for `telemetry_endpoint`. What that service is,\nwhat it stores, and what it structurally cannot store is spelled out in \"What\nthe endpoint does\" below.\n\n**To send nothing anywhere, turn telemetry off** (see \"Turning it off\"). To stay\nenabled but contact nobody, set `telemetry_endpoint = \"\"`: batches are then\nappended to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` on your own machine, byte\nfor byte what the server would have received, and no HTTP client is ever\nconstructed. That file is how you audit this document against reality.\n\nThis document is the schema. It is not a summary of the schema: a test in\n`crates/telemetry` parses the field names out of this file and asserts set\nequality against the structs the serializer actually uses, so a field that is\nhere and not in the code — or in the code and not here — fails the build.\n\nLocal saved sessions and `codewhale metrics` remain available with telemetry\ndisabled and require no account or hosted reporting service. Disabling telemetry\ndoes not disable local usage receipts.\n\n## Turning it off\n\nThere are two off switches and they do different things. Both stop collection\ncompletely; only one of them erases anything.\n\n```sh\ncodewhale config set telemetry false # opt out: stops collection and erases state\nCODEWHALE_TELEMETRY=0 codewhale # kill switch: stops collection, erases nothing\ncodewhale --telemetry false # the same kill switch, for one command\n```\n\n**`telemetry = false` in the config file is the opt-out.** It is a floor:\n`--telemetry true` and `CODEWHALE_TELEMETRY=1` both lose to it, because a\nsetting you can undo by accident from a wrapper script is not a setting. It\ndeletes the random install id, truncates every buffered event and every dry-run\nrecord, and writes a tombstone. Appends, identity/state writes, and delivery all\nshare the wipe's ordering lock, so once opt-out returns no pre-opt-out write or\nPOST remains in flight. If any part of that wipe fails, the tombstone is still\nthere and the buffer is undrainable — a failed wipe fails closed. Every later\nrun re-asserts the same tombstone for as long as the setting stands, so it\nsurvives. Turning telemetry back on in `/settings` records that explicit preference\nin both existing privacy registers; a fresh launch can then clear the tombstone. Nothing buffered before that point is\never sent.\n\n**The environment variable and the flag are kill switches, not opt-outs.**\nTelemetry is off for the run, nothing is written, nothing is sent — and\nnothing on disk is touched or deleted. That is deliberate: a harness or agent\nthat sets `CODEWHALE_TELEMETRY=0` for one command must not silently discard the\ninstall id and the dry-run records of the person who owns the machine. If you\nwant the erasing kind, use the config file.\n\n`CODEWHALE_TELEMETRY` (and its `DEEPSEEK_TELEMETRY` alias) accepts\n`0`, `1`, `true`, `false`, `yes`, `no`, Line truncated
|
||||
"sha256": "ead511b17a4986600cbe2effc9d4d91b4743958e9ef0cf0fe24ae873a58173a2",
|
||||
"content": "# Codewhale product telemetry\n\n> 阅读简体中文版:[zh_hans/TELEMETRY.md](zh_hans/TELEMETRY.md)\n\n**Usage analytics are on by default in the current source.** The first\ninteractive launch gives a localized, nonblocking disclosure naming **Codewhale\nand PostHog**, with a direct route to turn counting off. Notice version `5`\ndescribes this policy; displaying it is not recorded as human acceptance.\nMissing an older acceptance record does not turn a fresh installation off.\nExisting explicit opt-outs remain off across upgrades, and unreadable privacy\nstate fails closed. Environment and CLI kill switches still take precedence.\n\nRead the disclosure with `codewhale config telemetry`. Turn usage off in\n`/settings` or run `codewhale config set telemetry false`. Turning it back on\nin Settings or with `codewhale config set telemetry true` updates the existing\ndurable preference and privacy records for new sessions. The compatibility\ncommand `codewhale config telemetry --accept-notice 5` remains available, but\nis not required for a fresh installation. Collection never includes work content or credentials.\n\nThe PostHog sink is optional and inert until the ingest operator explicitly\nconfigures a project token and approved regional host, and records staging\nproof that the actual egress path does not forward the original client IP.\nSource readiness does not establish a deployment, an activated processor,\nan egress receipt, or a retention setting.\n\nCodewhale does not collect conversations, code, prompts, files, file/repo/branch\nnames, model content, or credentials. It sends no per-turn or per-tool timeline.\nIt does send the closed, aggregate schema below: version and platform classes,\nsession duration/outcome, feature/error counters, and a random install id that\nrotates every 90 days.\n\n**There is now a real endpoint.** An enabled session sends its batches to the\nfirst-party ingest service at `https://telemetry.codewhale.net/v1/telemetry`,\nwhich is the shipped default for `telemetry_endpoint`. What that service is,\nwhat it stores, and what it structurally cannot store is spelled out in \"What\nthe endpoint does\" below.\n\n**To send nothing anywhere, turn telemetry off** (see \"Turning it off\"). To stay\nenabled but contact nobody, set `telemetry_endpoint = \"\"`: batches are then\nappended to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` on your own machine, byte\nfor byte what the server would have received, and no HTTP client is ever\nconstructed. That file is how you audit this document against reality.\n\nThis document is the schema. It is not a summary of the schema: a test in\n`crates/telemetry` parses the field names out of this file and asserts set\nequality against the structs the serializer actually uses, so a field that is\nhere and not in the code — or in the code and not here — fails the build.\n\nLocal saved sessions and `codewhale metrics` remain available with telemetry\ndisabled and require no account or hosted reporting service. Disabling telemetry\ndoes not disable local usage receipts.\n\n## Turning it off\n\nThere are two off switches and they do different things. Both stop collection\ncompletely; only one of them erases anything.\n\n```sh\ncodewhale config set telemetry false # opt out: stops collection and erases state\nCODEWHALE_TELEMETRY=0 codewhale # kill switch: stops collection, erases nothing\ncodewhale --telemetry false # the same kill switch, for one command\n```\n\n**`telemetry = false` in the config file is the opt-out.** It is a floor:\n`--telemetry true` and `CODEWHALE_TELEMETRY=1` both lose to it, because a\nsetting you can undo by accident from a wrapper script is not a setting. It\ndeletes the random install id, truncates every buffered event and every dry-run\nrecord, and writes a tombstone. Appends, identity/state writes, and delivery all\nshare the wipe's ordering lock, so once opt-out returns no pre-opt-out write or\nPOST remains in flight. If any part of that wipe fails, the tombstone is still\nthere and the buffer is undrainable — a failed wipe fails closed. Every later\nrun re-asserts the same tombstone for as long as the setting stands, so it\nsurvives. Turning telemetry back on in `/settings` records that explicit preference\nin both existing privacy registers; a fresh launch can then clear the tombstone. Nothing buffered before that point is\never sent.\n\n**The environment variable and the flag are kill switches, not opt-outs.**\nTelemetry is off for the run, nothing is written, nothing is sent — and\nnothing on disk is touched or deleted. That is deliberate: a harness or agent\nthat sets `CODEWHALE_TELEMETRY=0` for one command must not silently discard the\ninstall id and the dry-run records of the person who owns the machine. If you\nwant the erasing kind, use the config file.\n\n`CODEWHALE_TELEMETRY` (and its `DEEPSEEK_TELEMETRY` alias) accepts\n`0`, `1`, `true`, `false`, `yes`, `no`, Line truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/public-surface-facts.json",
|
||||
@@ -208,8 +208,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/MCP.md",
|
||||
"sha256": "f9516ede4e2b90239069600c78a36070f89410cb5bd40b8d9b7d1926303de0a5",
|
||||
"content": "# MCP (External Tool Servers)\n\nIn the terminal, `/mcp` (also `/mcps`) opens **Extensions → MCP**. Enter opens the selected server’s recovery action or read-only details. An empty inventory offers server suggestions; browsing them installs nothing. Explicit subcommands such as `/mcp status`, `/mcp doctor`, `/mcp login`, and `/mcp add` retain their existing behavior.\n\n\n> 阅读简体中文版:[zh_hans/MCP.md](zh_hans/MCP.md)\n\ncodewhale can load additional tools via MCP (Model Context Protocol). MCP servers can be local stdio processes that the TUI starts, or remote URL-based servers that speak Streamable HTTP with legacy SSE fallback.\n\nBrowsing note:\n- `Web` is the canonical, deferred built-in browsing tool; it provides\n `search`, `fetch`, and `wait` actions when network policy permits.\n- `web_search`, `fetch_url`, and `wait_for_dev_server` are hidden replay-only\n aliases. New prompts and integrations should use `Web`.\n\nServer mode note:\n- `codewhale serve --mcp` runs the MCP stdio server.\n- `codewhale serve --http` runs the runtime HTTP/SSE API (separate mode).\n- `codewhale mcp-server` is an equivalent stdio entrypoint on the same\n consolidated runtime.\n\nIn 0.10.1, the old child-server aggregation proxy is removed. `mcp-server`\nnow uses the same native tool server as `serve --mcp`; it does not launch the\nlegacy `mcp.server_definitions` list. Saved definitions are left intact. Configure\nexternal servers in `mcp.json` for the existing MCP client, or connect to those\nservers directly from your external client. Native server write tools remain\nwithheld unless the operator explicitly permits them in its server configuration.\n\nThe native stdio server supports MCP **2026-07-28** with required per-request\nmetadata and `server/discover`; no initialization handshake is needed for that\nrevision. It also supports the explicit legacy revisions **2024-11-05**,\n**2025-03-26**, **2025-06-18** and **2025-11-25** through `initialize`.\nInitialization never selects a modern revision; unsupported initialization\nversions fall back to 2025-11-25. Legacy callers should send\n`notifications/initialized`, but calls may proceed after a valid initialize\nresponse. This describes Codewhale as a server; outbound MCP client version\nsupport is configured separately.\n\n`resources/list` pages through the effective native skill instruction catalog,\nand `resources/read` returns the accepted instruction body for an exact listed\n`codewhale://skills/...` URI. Discovery follows the configured skill roots,\nprecedence and workspace trust; disabled skills are omitted. Reads supply inert\nMarkdown and never activate a skill. These are instruction-body resources, not\noriginal SKILL.md files or complete skill packages. Plugin skills, arbitrary\nfilesystem paths and session transcripts are not exposed by this catalog.\nThe old workspace/session entries, which had no resource reader, are removed.\nResource templates are empty. Lists and reads use private caching with a zero\nTTL; restart pagination if a changed catalog invalidates a cursor.\n\nOnly tools and resources are advertised. Subscriptions, list-change\nnotifications, the Skills extension, Apps, tasks and interactive approval are\nnot implemented by this server. Tool execution remains serialized through the\nRust registry, and request cancellation reaches the existing tool context.\nKeep stdin open until responses arrive; closing it cancels outstanding work.\nMalformed JSON and oversized input frames are discarded by the bounded SDK\ntransport, while well-formed invalid JSON-RPC receives an error.\n\n## Setup wizard vs manual MCP setup (#3407)\n\nThe `/setup` hub includes an optional **Tools and MCP**\nstep. That step is discovery/readiness only:\n\n| Wizard can do | Still requires manual / explicit action |\n| --- | --- |\n| Show configured servers as `healthy` / `needs_config` / `off` | Start or connect MCP servers |\n| Report config path presence (global + project) | Write or edit `mcp.json` contents |\n| Safe static health probe (missing command/url, broken absolute path, missing bearer env) | `codewhale mcp validate`, live connect, OAuth login |\n| Point at safe on-ramps (`/mcp`, `codewhale mcp init`, `codewhale doctor`) | Install community skills, trust skills, enable plugins |\n| Share Hotbar source counts from the same skill/MCP adapters (#3399) | Bind Hotbar slots (Hotbar step / `H`) |\n| Record optional/`needs_action` setup_state without blocking first-run | Anything that spawns processes or installs packages |\n\nEmpty inventory is **not** an error: first-run users see “nothing configured\nyet, that’s fine.” Failing or incomplete configured servers surface as\n`needs_config` with an actionable hint and never block setup completion.\nEnumeration never executes MCP/plugin commands beyond the static probe.\nSummaries redact commands, args, env, headers, and tokens.\n\n`codewhale doctor` reports MCP/skills/tools/plugLine truncated
|
||||
"sha256": "ee78f41afbe898dc7fd8b782a9055ecee4b90cb6c66a841a3a35d839a5474465",
|
||||
"content": "# MCP (External Tool Servers)\n\nIn the terminal, `/mcp` (also `/mcps`) opens **Extensions → MCP**. Enter opens the selected server’s recovery action or read-only details. An empty inventory offers server suggestions; browsing them installs nothing. Explicit subcommands such as `/mcp status`, `/mcp doctor`, `/mcp login`, and `/mcp add` retain their existing behavior.\n\n\n> 阅读简体中文版:[zh_hans/MCP.md](zh_hans/MCP.md)\n\ncodewhale can load additional tools via MCP (Model Context Protocol). MCP servers can be local stdio processes that the TUI starts, or remote URL-based servers that speak Streamable HTTP with legacy SSE fallback.\n\nBrowsing note:\n- `Web` is the canonical, deferred built-in browsing tool; it provides\n `search`, `fetch`, and `wait` actions when network policy permits.\n- `web_search`, `fetch_url`, and `wait_for_dev_server` are hidden replay-only\n aliases. New prompts and integrations should use `Web`.\n\nServer mode note:\n- `codewhale serve --mcp` runs the MCP stdio server.\n- `codewhale serve --http` runs the runtime HTTP/SSE API (separate mode).\n- `codewhale mcp-server` is an equivalent stdio entrypoint on the same\n consolidated runtime.\n\nIn 0.10.1, the old child-server aggregation proxy is removed. `mcp-server`\nnow uses the same native tool server as `serve --mcp`; it does not launch the\nlegacy `mcp.server_definitions` list. Saved definitions are left intact. Configure\nexternal servers in `mcp.json` for the existing MCP client, or connect to those\nservers directly from your external client. Native server write tools remain\nwithheld unless the operator explicitly permits them in its server configuration.\n\nThe native stdio server supports MCP **2026-07-28** with required per-request\nmetadata and `server/discover`; no initialization handshake is needed for that\nrevision. It also supports the explicit legacy revisions **2024-11-05**,\n**2025-03-26**, **2025-06-18** and **2025-11-25** through `initialize`.\nInitialization never selects a modern revision; unsupported initialization\nversions fall back to 2025-11-25. Legacy callers should send\n`notifications/initialized`, but calls may proceed after a valid initialize\nresponse. This describes Codewhale as a server; outbound MCP client version\nsupport is configured separately.\n\n`resources/list` pages through the effective native skill instruction catalog,\nand `resources/read` returns the accepted instruction body for an exact listed\n`codewhale://skills/...` URI. Discovery follows the configured skill roots,\nprecedence and workspace trust; disabled skills are omitted. Reads supply inert\nMarkdown and never activate a skill. These are instruction-body resources, not\noriginal SKILL.md files or complete skill packages. Plugin skills, arbitrary\nfilesystem paths and session transcripts are not exposed by this catalog.\nThe old workspace/session entries, which had no resource reader, are removed.\nResource templates are empty. Lists and reads use private caching with a zero\nTTL; restart pagination if a changed catalog invalidates a cursor.\n\nOnly tools and resources are advertised. Subscriptions, list-change\nnotifications, the Skills extension, Apps, tasks and interactive approval are\nnot implemented by this server. Tool execution remains serialized through the\nRust registry, and request cancellation reaches the existing tool context.\nKeep stdin open until responses arrive; closing it cancels outstanding work.\nMalformed JSON and oversized input frames are discarded by the bounded SDK\ntransport, while well-formed invalid JSON-RPC receives an error.\n\n## Setup wizard vs manual MCP setup (#3407)\n\nThe `/setup` hub includes an optional **Tools and MCP**\nstep. That step is discovery/readiness only:\n\n| Wizard can do | Still requires manual / explicit action |\n| --- | --- |\n| Show configured servers as `healthy` / `needs_config` / `off` | Start or connect MCP servers |\n| Report config path presence (global + project) | Write or edit `mcp.json` contents |\n| Safe static health probe (missing command/url, broken absolute path, missing bearer env) | `codewhale mcp validate`, live connect, OAuth login |\n| Point at safe on-ramps (`/mcp`, `codewhale mcp init`, `codewhale doctor`) | Install community skills, trust skills, enable plugins |\n| Share Hotbar source counts from the same skill/MCP adapters (#3399) | Bind Hotbar slots (Hotbar step / `H`) |\n| Record optional/`needs_action` setup_state without blocking first-run | Anything that spawns processes or installs packages |\n\nEmpty inventory is **not** an error: first-run users see “nothing configured\nyet, that’s fine.” Failing or incomplete configured servers surface as\n`needs_config` with an actionable hint and never block setup completion.\nEnumeration never executes MCP/plugin commands beyond the static probe.\nSummaries redact commands, args, env, headers, and tokens.\n\n`codewhale doctor` reports MCP/skills/tools/plugLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -224,8 +224,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "3e076746af15d2c2f25bd60d86648c95ca17fc98ec0d9b1bd4d3a025958dd5f3",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
|
||||
"sha256": "c36db36c85ad3be7d11a01dbdf0c74a5641844a62169431db5988a127a34cd69",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -256,8 +256,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/CONFIGURATION.md",
|
||||
"sha256": "3e076746af15d2c2f25bd60d86648c95ca17fc98ec0d9b1bd4d3a025958dd5f3",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
|
||||
"sha256": "c36db36c85ad3be7d11a01dbdf0c74a5641844a62169431db5988a127a34cd69",
|
||||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -277,8 +277,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/EXTENSIONS.md",
|
||||
"sha256": "5bf540a2a1a1740e3ae251cfed8eb7066e03675934b4cdb57a9dd12a4ac48717",
|
||||
"content": "# Writing an extension tool or command\n\nThe experimental TypeScript extension host runs reviewed plugin code in a\nshared Node process (or, as an opt-in, Bun). Rust still owns sessions, tool admission, approval and\nexecution. Extensions currently contribute tools and slash commands; they\ncannot provide an approval service, run a second agent loop or replace\nbuilt-in tools or commands.\n\n## Quick start\n\nStart with [hello-extension](examples/plugins/hello-extension/hello.mts) and its\n[plugin.json](examples/plugins/hello-extension/plugin.json). The example is\nexecuted unchanged by the host tests. It returns a greeting and does not read\nfiles or use the network.\n\n1. Explicitly enable `[features] extension_host = true` in your configuration,\n or start Codewhale with `codewhale --enable extension_host` (the flag goes\n before any subcommand).\n2. Install the example's directory using `/plugin install <local-directory>`.\n3. Run `/plugin validate hello-extension`, then `/plugin show hello-extension`.\n4. Run `/plugin enable hello-extension` to open its content/capability review.\n Read the review and personally run its exact `/plugin trust <token>` command,\n then enable the plugin. Trust alone does not execute the code.\n5. Ask Codewhale to use `hello_greet`. The tool follows the normal approval gate.\n6. Run `/hello-greet Codewhale`. The command runs when you type it, and shows\n its answer in the transcript with its origin, `extension:hello-extension`.\n\nAn authoring agent should stop after installation and validation and present\nthe review to the person. Do not trust or enable a bundle automatically.\n\n## Locations, installation and trust\n\nUser bundles live under `~/.codewhale/plugins/`; workspace bundles live under\n`<workspace>/.codewhale/plugins/`. New bundles use Agent Plugins v1.0.0\n`plugin.json`. Declare `extensions[\"net.codewhale\"].native.path` as one regular\n`.mjs`, `.js` or `.mts` file inside the bundle. Directories and other extensions\nfail validation when the host is enabled. With the flag off, native entries\nremain inventory-only.\n\nA plugin may split its code across several entries with `native.paths` (up to\n64, alone or beside `path`). They are activated in the order declared, as the\nfibers of one plugin: they share one owner, so one disable, review change or\ncrash tears all of them down together. Each entry is a separate module with its\nown `apply`, and tool and command names must be unique across them. If an entry\nfails to activate (throws, requires a service the host does not provide, or has\na registration refused), only that entry is withdrawn: entries that already\nactivated stay registered, later entries still activate, and the plugin fails\nonly when no entry activates. Listing the same file twice is one entry.\n\n## Runtime\n\n`[extension_host] runtime` selects what runs the host. Node is the default.\nBun is an opt-in: it is qualified on macOS only, and becomes the default only\nafter an explicit, recorded cutover.\n\n```toml\n[extension_host]\nruntime = \"node\" # default: Node only\n# runtime = \"bun\" # Bun only; fails rather than falling back to Node\n# runtime = \"auto\" # Bun >= 1.4.0 if one is found and starts, otherwise Node\n# bun = \"~/.bun/bin/bun\" # the only Bun tried when set\n# node = \"/opt/homebrew/bin/node\" # the only Node tried when set\n# mcp_backend = \"rust\" # temporary opt-out; default is \"host\"\n```\n\nThe default `mcp_backend = \"host\"` uses the pinned MCP SDK independently of\n`[features] extension_host`, which controls optional Native extensions. Existing\nstdio and HTTP servers use modern discovery when supported; explicitly configured\nlegacy SSE servers use their legacy handshake. Rust retains process and network\nauthority, credential resolution, catalog admission, tool approval and cancellation.\nThe SDK receives opaque session selectors and exact operation grants. Modern\nrequests carry Rust-approved protocol metadata and headers without a session id.\n\nA typed discovery refusal can admit a legacy handshake. Only an explicit HTTP\ntransport refusal during that handshake can admit legacy SSE with a fresh exact\nRust grant. Legacy HTTP retains its GET preflight and bounded OAuth refresh;\nmodern HTTP does not inherit those session or resend behaviors. Incomplete result\nflows are reported without automatically repeating the operation. Credentials,\nconfigured URLs, OAuth browser/login operations and Computer Use decision keys\nstay in Rust.\n\nBoth backends share recorded semantic MCP goldens and retain separate transport\ngoldens, including discovery, authentication and session evidence. The Host default\nrequires those fixtures and the actual broker acceptance checks, plus the platform\nisolation and measured gates in Ops CURRENT_DECISIONS §26. An explicit\n`mcp_backend = \"rust\"` remains available for one release after the qualified default\nflip, before the native protocoLine truncated
|
||||
"sha256": "2f8c5d104925bd9fcc513aea21e1f546f46cbe9a98f25e1f5bceaf346fa3886f",
|
||||
"content": "# Writing an extension tool or command\n\nThe experimental TypeScript extension host runs reviewed plugin code in a\nshared Node process (or, as an opt-in, Bun). Rust still owns sessions, tool admission, approval and\nexecution. Extensions currently contribute tools and slash commands; they\ncannot provide an approval service, run a second agent loop or replace\nbuilt-in tools or commands.\n\n## Quick start\n\nStart with [hello-extension](examples/plugins/hello-extension/hello.mts) and its\n[plugin.json](examples/plugins/hello-extension/plugin.json). The example is\nexecuted unchanged by the host tests. It returns a greeting and does not read\nfiles or use the network.\n\n1. Explicitly enable `[features] extension_host = true` in your configuration,\n or start Codewhale with `codewhale --enable extension_host` (the flag goes\n before any subcommand).\n2. Install the example's directory using `/plugin install <local-directory>`.\n3. Run `/plugin validate hello-extension`, then `/plugin show hello-extension`.\n4. Run `/plugin enable hello-extension` to open its content/capability review.\n Read the review and personally run its exact `/plugin trust <token>` command,\n then enable the plugin. Trust alone does not execute the code.\n5. Ask Codewhale to use `hello_greet`. The tool follows the normal approval gate.\n6. Run `/hello-greet Codewhale`. The command runs when you type it, and shows\n its answer in the transcript with its origin, `extension:hello-extension`.\n\nAn authoring agent should stop after installation and validation and present\nthe review to the person. Do not trust or enable a bundle automatically.\n\n## Locations, installation and trust\n\nUser bundles live under `~/.codewhale/plugins/`; workspace bundles live under\n`<workspace>/.codewhale/plugins/`. New bundles use Agent Plugins v1.0.0\n`plugin.json`. Declare `extensions[\"net.codewhale\"].native.path` as one regular\n`.mjs`, `.js` or `.mts` file inside the bundle. Directories and other extensions\nfail validation when the host is enabled. With the flag off, native entries\nremain inventory-only.\n\nA plugin may split its code across several entries with `native.paths` (up to\n64, alone or beside `path`). They are activated in the order declared, as the\nfibers of one plugin: they share one owner, so one disable, review change or\ncrash tears all of them down together. Each entry is a separate module with its\nown `apply`, and tool and command names must be unique across them. If an entry\nfails to activate (throws, requires a service the host does not provide, or has\na registration refused), only that entry is withdrawn: entries that already\nactivated stay registered, later entries still activate, and the plugin fails\nonly when no entry activates. Listing the same file twice is one entry.\n\n## Runtime\n\n`[extension_host] runtime` selects what runs the host. Node is the default.\nBun is an opt-in: it is qualified on macOS only, and becomes the default only\nafter an explicit, recorded cutover.\n\n```toml\n[extension_host]\nruntime = \"node\" # default: Node only\n# runtime = \"bun\" # Bun only; fails rather than falling back to Node\n# runtime = \"auto\" # Bun >= 1.4.0 if one is found and starts, otherwise Node\n# bun = \"~/.bun/bin/bun\" # the only Bun tried when set\n# node = \"/opt/homebrew/bin/node\" # the only Node tried when set\n# mcp_backend = \"rust\" # temporary opt-out; default is \"host\"\n```\n\nThe default `mcp_backend = \"host\"` uses the pinned MCP SDK independently of\n`[features] extension_host`, which controls optional Native extensions. Existing\nstdio and HTTP servers use modern discovery when supported; explicitly configured\nlegacy SSE servers use their legacy handshake. Rust retains process and network\nauthority, credential resolution, catalog admission, tool approval and cancellation.\nThe SDK receives opaque session selectors and exact operation grants. Modern\nrequests carry Rust-approved protocol metadata and headers without a session id.\n\nA typed discovery refusal can admit a legacy handshake. Only an explicit HTTP\ntransport refusal during that handshake can admit legacy SSE with a fresh exact\nRust grant. Legacy HTTP retains its GET preflight and bounded OAuth refresh;\nmodern HTTP does not inherit those session or resend behaviors. Incomplete result\nflows are reported without automatically repeating the operation. Credentials,\nconfigured URLs, OAuth browser/login operations and Computer Use decision keys\nstay in Rust.\n\nBoth backends share recorded semantic MCP goldens and retain separate transport\ngoldens, including discovery, authentication and session evidence. The Host default\nrequires those fixtures and the actual broker acceptance checks, plus the platform\nisolation and measured gates in Ops CURRENT_DECISIONS §26. An explicit\n`mcp_backend = \"rust\"` remains available for one release after the qualified default\nflip, before the native protocoLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -288,8 +288,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/RUNTIME_API.md",
|
||||
"sha256": "dbb3beebdb8395dd7ccf9fb125d9a767cd404b0565fd36b6131c7f5f0e501268",
|
||||
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. Bare `app-server`, `--stdio` and `--socket` select smaller\ncompatibility interfaces, not the full Engine API.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | JSON-RPC 2.0 over stdio | Compatibility controls and explicit tool-free turns; no full Engine event or approval/input channel. |\n| `codewhale app-server --socket [--socket-path P]` | JSON-RPC 2.0 over a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | HTTP on `127.0.0.1:8787` | Compatibility `/healthz`, `/thread`, `/app`, `/prompt`. Turn requests require explicit `allowed_tools: []`; interactive execution fails before a turn starts. Use `app-server --http` for tools, approvals, clarification, goal execution, steering and the full event journal. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nThe legacy in-process `codewhale app-server` also requires an explicit\n`--auth-token` or `CODEWHALE_APP_SERVER_TOKEN` before binding a non-loopback\nhost; its generated one-time `cwapp_*` token is loopback-only.\n\nDevice tokens minted by the master through `POST /v1/auth/client-tokens`\nhave an immutable `intent`: `watch` (the default when omitted) or explicit\n`drive`. Labels do not grant authority. Watch permits ordinary GET/HEAD reads,\nComputer displayLine truncated
|
||||
"sha256": "7380f415b65e7115930f9108424c89ed7cc849a57622cbf32520225dac7ad27c",
|
||||
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. Bare `app-server`, `--stdio` and `--socket` select smaller\ncompatibility interfaces, not the full Engine API.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | JSON-RPC 2.0 over stdio | Compatibility controls and explicit tool-free turns; no full Engine event or approval/input channel. |\n| `codewhale app-server --socket [--socket-path P]` | JSON-RPC 2.0 over a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | HTTP on `127.0.0.1:8787` | Compatibility `/healthz`, `/thread`, `/app`, `/prompt`. Turn requests require explicit `allowed_tools: []`; interactive execution fails before a turn starts. Use `app-server --http` for tools, approvals, clarification, goal execution, steering and the full event journal. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nThe legacy in-process `codewhale app-server` also requires an explicit\n`--auth-token` or `CODEWHALE_APP_SERVER_TOKEN` before binding a non-loopback\nhost; its generated one-time `cwapp_*` token is loopback-only.\n\nDevice tokens minted by the master through `POST /v1/auth/client-tokens`\nhave an immutable `intent`: `watch` (the default when omitted) or explicit\n`drive`. Labels do not grant authority. Watch permits ordinary GET/HEAD reads,\nComputer displayLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -326,8 +326,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "CONTRIBUTING.md",
|
||||
"sha256": "e6ce0e044f2e61f290fdffee83879e965b4c6fc40f8d61f9f2b2037e692cfa79",
|
||||
"content": "# Contributing to codewhale\n\nThank you for your interest in contributing to codewhale! This document provides guidelines and instructions for contributing.\n\n## Getting Started\n\n### Prerequisites\n\n- Rust 1.89 or later (edition 2024)\n- Cargo package manager\n- Git\n\n### Setting Up Development Environment\n\n1. Fork and clone the repository:\n ```bash\n git clone https://github.com/YOUR_USERNAME/CodeWhale.git\n cd CodeWhale\n ```\n\n2. Build the current engine and terminal:\n ```bash\n CODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --locked -p codewhale-cli\n ```\n\n3. Run the tests near your change (see [Fast local loop](#fast-local-loop)):\n ```bash\n scripts/dev-test.sh tui your_test_filter\n ```\n\n4. Run with development settings:\n ```bash\n ./target/debug/codewhale --version\n ./target/debug/codewhale\n ```\n\n### Testing the latest source\n\nThe canonical source is [`codewhale-hq/Codewhale`'s `main` branch](https://github.com/codewhale-hq/Codewhale/tree/main).\nRelease candidates land there after their CI gates pass, so contributors can\ntest and build on the same source. Tagged downloads remain the latest published\nrelease; their version can lag the development version on `main`.\n\nFor a fresh checkout of the current source:\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/Codewhale.git\ncd Codewhale\nCODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --release --locked -p codewhale-cli\n./target/release/codewhale --version\n./target/release/codewhale\n```\n\nInclude the commit shown by `--version` when reporting a problem. If you work\nfrom a fork, add the canonical repository as `upstream` and fetch `upstream/main`\nbefore starting a change; preserve any local work when updating your branch.\n\n## Development Workflow\n\n### Code Style\n\n- Run `cargo fmt` before committing to ensure consistent formatting\n- Run `cargo clippy` and address all warnings\n- Follow Rust naming conventions (snake_case for functions/variables, CamelCase for types)\n- Add documentation comments for public APIs\n\n### Testing\n\n- Write tests for new functionality\n- Run the tests near your change (see [Fast local loop](#fast-local-loop));\n your pull request runs the Linux workspace suite and a Windows build for\n you (see [What happens after you open a pull request](#what-happens-after-you-open-a-pull-request))\n- Colocate unit tests beside the code they cover (standard Rust `#[cfg(test)]`\n modules), and add integration tests under the owning crate's `tests/`\n directory (for example `crates/tui/tests/` or `crates/state/tests/`). The\n repository root `tests/` directory is not used\n\n### Pre-push verification\n\nThese are the Rust commands your pull request runs on Linux (the `Lint` and\n`Test (ubuntu-latest)` checks). You do not need all of them before every\npush: run `cargo fmt`, then check and test the crates you touched (see\n[Fast local loop](#fast-local-loop)). Run the full set locally when a change\nspans many crates, or let the pull request run it for you:\n\n```bash\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets --all-features --locked -- \\\n -D warnings \\\n -A clippy::uninlined_format_args \\\n -A clippy::too_many_arguments \\\n -A clippy::unnecessary_map_or\ncargo test --workspace --all-features --locked\n```\n\nClippy lints test, bench and example targets too (`--all-targets`), and the\nthree allowances above are the only ones CI and the release lane accept.\n\n#### Fast local loop\n\nThe full gate above is what CI enforces, but you do not need it for every\nedit. `crates/tui` is a ~750k-line crate, so the loop that stays fast is\nthe one that avoids rebuilding it more than necessary (numbers and the\nreasoning are in [`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md)):\n\n```bash\n# 1. Type-check first (seconds after the first build; no codegen, no link).\nscripts/dev-cargo.sh check -p codewhale-tui\n\n# 2. Run only the tests near your change (one crate, one filter).\nscripts/dev-test.sh tui fleet_setup\n# or: scripts/dev-test.sh crates/command-contract/src/elapsed.rs\n\n# 3. Run a whole crate's unit suite. scripts/dev-test.sh uses nextest when\n# it is installed (one process per test, all cores busy, slow tests\n# named; ~100 s here vs ~270 s with libtest).\ncargo install cargo-nextest --locked # once\nscripts/dev-test.sh tui\nscripts/dev-cargo.sh nextest run --workspace --all-features --locked\n\n# 4. The Linux suite your PR runs:\ncargo test --workspace --all-features --locked\n```\n\n`.config/nextest.toml` already serializes the PTY suite and bounds the\nintegration tests that spawn the real binary, so `cargo nextest run` is\nsafe to use on the whole workspace (nextest does not run doctests; the\nauthoritative `cargo test` gate does). Tests must not depend on running in\nthe same process as another test (nextest gives every test its own\nprocess); if a test Line truncated
|
||||
"sha256": "5a13b836a4e56ce49672daf297abe93476fdc95316ccc54bf6c43d516b15e83a",
|
||||
"content": "# Contributing to codewhale\n\nThank you for your interest in contributing to codewhale! This document provides guidelines and instructions for contributing.\n\n## Getting Started\n\n### Prerequisites\n\n- Rust 1.89 or later (edition 2024)\n- Cargo package manager\n- Git\n\n### Setting Up Development Environment\n\n1. Fork and clone the repository:\n ```bash\n git clone https://github.com/YOUR_USERNAME/CodeWhale.git\n cd CodeWhale\n ```\n\n2. Build the current engine and terminal:\n ```bash\n CODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --locked -p codewhale-cli\n ```\n\n3. Run the tests near your change (see [Fast local loop](#fast-local-loop)):\n ```bash\n scripts/dev-test.sh tui your_test_filter\n ```\n\n4. Run with development settings:\n ```bash\n ./target/debug/codewhale --version\n ./target/debug/codewhale\n ```\n\n### Testing the latest source\n\nThe canonical source is [`codewhale-hq/Codewhale`'s `main` branch](https://github.com/codewhale-hq/Codewhale/tree/main).\nRelease candidates land there after their CI gates pass, so contributors can\ntest and build on the same source. Tagged downloads remain the latest published\nrelease; their version can lag the development version on `main`.\n\nFor a fresh checkout of the current source:\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/Codewhale.git\ncd Codewhale\nCODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --release --locked -p codewhale-cli\n./target/release/codewhale --version\n./target/release/codewhale\n```\n\nInclude the commit shown by `--version` when reporting a problem. If you work\nfrom a fork, add the canonical repository as `upstream` and fetch `upstream/main`\nbefore starting a change; preserve any local work when updating your branch.\n\n## Development Workflow\n\n### Code Style\n\n- Run `cargo fmt` before committing to ensure consistent formatting\n- Run `cargo clippy` and address all warnings\n- Follow Rust naming conventions (snake_case for functions/variables, CamelCase for types)\n- Add documentation comments for public APIs\n\n### Testing\n\n- Write tests for new functionality\n- Run the tests near your change (see [Fast local loop](#fast-local-loop));\n your pull request runs the Linux workspace suite and a Windows build for\n you (see [What happens after you open a pull request](#what-happens-after-you-open-a-pull-request))\n- Colocate unit tests beside the code they cover (standard Rust `#[cfg(test)]`\n modules), and add integration tests under the owning crate's `tests/`\n directory (for example `crates/tui/tests/` or `crates/state/tests/`). The\n repository root `tests/` directory is not used\n\n### Pre-push verification\n\nThese are the Rust commands your pull request runs on Linux (the `Lint` and\n`Test (ubuntu-latest)` checks). You do not need all of them before every\npush: run `cargo fmt`, then check and test the crates you touched (see\n[Fast local loop](#fast-local-loop)). Run the full set locally when a change\nspans many crates, or let the pull request run it for you:\n\n```bash\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets --all-features --locked -- \\\n -D warnings \\\n -A clippy::uninlined_format_args \\\n -A clippy::too_many_arguments \\\n -A clippy::unnecessary_map_or\ncargo test --workspace --all-features --locked\n```\n\nClippy lints test, bench and example targets too (`--all-targets`), and the\nthree allowances above are the only ones CI and the release lane accept.\n\n#### Fast local loop\n\nThe full gate above is what CI enforces, but you do not need it for every\nedit. `crates/tui` is a ~750k-line crate, so the loop that stays fast is\nthe one that avoids rebuilding it more than necessary (numbers and the\nreasoning are in [`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md)):\n\n```bash\n# 1. Type-check first (seconds after the first build; no codegen, no link).\nscripts/dev-cargo.sh check -p codewhale-tui\n\n# 2. Run only the tests near your change (one crate, one filter).\nscripts/dev-test.sh tui fleet_setup\n# or: scripts/dev-test.sh crates/command-contract/src/elapsed.rs\n\n# 3. Run a whole crate's unit suite. scripts/dev-test.sh uses nextest when\n# it is installed (one process per test, all cores busy, slow tests\n# named; ~100 s here vs ~270 s with libtest).\ncargo install cargo-nextest --locked # once\nscripts/dev-test.sh tui\nscripts/dev-cargo.sh nextest run --workspace --all-features --locked\n\n# 4. The Linux suite your PR runs:\ncargo test --workspace --all-features --locked\n```\n\n`.config/nextest.toml` already serializes the PTY suite and bounds the\nintegration tests that spawn the real binary, so `cargo nextest run` is\nsafe to use on the whole workspace (nextest does not run doctests; the\nauthoritative `cargo test` gate does). Tests must not depend on running in\nthe same process as another test (nextest gives every test its own\nprocess); if a test Line truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/AGENT_ETHOS.md",
|
||||
@@ -336,8 +336,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/CONTRIBUTORS.md",
|
||||
"sha256": "2a0a80012465c19a3d2ef1d3aeb48547381c9cd965379db21950dfe0c3aa11cc",
|
||||
"content": "# Contributors\n\nCodewhale is built in the open with a growing community of contributors. Every\nissue report and pull request is real project work — welcome at any experience\nlevel. This is the full per-PR contributor record, grouped into release/time\nbands with the newest band first so it stays scannable. Expand any band to see\neveryone.\n\nFor the live list, see the\n[GitHub contributors page](https://github.com/codewhale-hq/CodeWhale/graphs/contributors),\n[`AUTHOR_MAP`](https://github.com/codewhale-hq/CodeWhale/blob/main/.github/AUTHOR_MAP),\nand [CHANGELOG.md](../CHANGELOG.md).\n\n<!-- contributor-wall:start -->\n## Everyone\n\n[](#everyone)\n\n421 people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts 203 of them. If your commits are missing there, add your commit email to your GitHub account and they will link.\n\n[0gl20shk0sbt36](https://github.com/0gl20shk0sbt36) · [1Git2Clone](https://github.com/1Git2Clone) · [20bytes](https://github.com/20bytes) · [47Cid](https://github.com/47Cid) · [7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) · [8sero](https://github.com/8sero) · [A-Corner](https://github.com/A-Corner) · [aboimpinto](https://github.com/aboimpinto) · [adity982](https://github.com/adity982) · [AdityaVG13](https://github.com/AdityaVG13) · [AGSaturn](https://github.com/AGSaturn) · [ai-dengwsh](https://github.com/ai-dengwsh) · [aiapienthusiast](https://github.com/aiapienthusiast) · [Aitensa](https://github.com/Aitensa) · [AiurArtanis](https://github.com/AiurArtanis) · [alitvak69](https://github.com/alitvak69) · [all-lopezg](https://github.com/all-lopezg) · [alvin1](https://github.com/alvin1) · [amuthantamil](https://github.com/amuthantamil) · [AnaheimEX](https://github.com/AnaheimEX) · [Andrea-Bruno](https://github.com/Andrea-Bruno) · [Angel-Hair](https://github.com/Angel-Hair) · [angus-guo](https://github.com/angus-guo) · [angziii](https://github.com/angziii) · [AnonymousUser443](https://github.com/AnonymousUser443) · [antarikshraya](https://github.com/antarikshraya) · [Anyexyz](https://github.com/Anyexyz) · [Apeiron0w0](https://github.com/Apeiron0w0) · [aqilaziz](https://github.com/aqilaziz) · [AresNing](https://github.com/AresNing) · [ArronAI007](https://github.com/ArronAI007) · [Artenx](https://github.com/Artenx) · [asdfg314284230](https://github.com/asdfg314284230) · [Asteria1028](https://github.com/Asteria1028) · [asto18089](https://github.com/asto18089) · [atmosuwiryo](https://github.com/atmosuwiryo) · [axobase001](https://github.com/axobase001) · [aznikline](https://github.com/aznikline) · [baendlorel](https://github.com/baendlorel) · [banqii](https://github.com/banqii) · [Beltran12138](https://github.com/Beltran12138) · [bevis-wong](https://github.com/bevis-wong) · [BH8GCJ](https://github.com/BH8GCJ) · [BigBenLabs](https://github.com/BigBenLabs) · [Biilow](https://github.com/Biilow) · [bistack](https://github.com/bistack) · [BrathonBai](https://github.com/BrathonBai) · [bruce6135](https://github.com/bruce6135) · [BryonGo](https://github.com/BryonGo) · [buiducnhat](https://github.com/buiducnhat) · [buko](https://github.com/buko) · [BX166](https://github.com/BX166) · [c020627](https://github.com/c020627) · [cacdcaecawae](https://github.com/cacdcaecawae) · [caeserchen](https://github.com/caeserchen) · [caiyilian](https://github.com/caiyilian) · [CCChisato](https://github.com/CCChisato) · [ccomma](https://github.com/ccomma) · [cenab](https://github.com/cenab) · [ChaceLyee2101](https://github.com/ChaceLyee2101) · [chatman-media](https://github.com/chatman-media) · [chennest](https://github.com/chennest) · [chinaqy110](https://github.com/chinaqy110) · [chnjames](https://github.com/chnjames) · [chuntseevolving](https://github.com/chuntseevolving) · [ci4ic4](https://github.com/ci4ic4) · [cian-lien](https://github.com/cian-lien) · [cmdcorp6534](https://github.com/cmdcorp6534) · [cmyyy](https://github.com/cmyyy) · [codepgq](https://github.com/codepgq) · [Colorful-glassblock](https://github.com/Colorful-glassblock) · [ComeFromTheMars](https://github.com/ComeFromTheMars) · [CrepuscularIRIS](https://github.com/CrepuscularIRIS) · [ct-jaryn](https://github.com/ct-jaryn) · [ctxyao](https://github.com/ctxyao) · [cy2311](https://github.com/cy2311) · [cyq1017](https://github.com/cyq1017) · [czf0718](https://github.com/czf0718) · [dajiaohuang](https://github.com/dajiaohuang) · [DarrellThomas](https://github.com/DarrellThomas) · [demian-welt](https://github.com/demian-welt) · [Dev-next-gen](https://github.com/Dev-next-gen) · [dfwqdyl-ui](https://github.com/dfwqdyl-ui) · [DI-HUO-MING-YI](https://github.com/DI-HUO-MING-YI) · [DieMoe233](https://github.com/DieMoe233) · [DingYong4223Line truncated
|
||||
"sha256": "a5e5d0d97f728e97ff02e6308d10a2f505a924bcafd2866be42e6aad834bc164",
|
||||
"content": "# Contributors\n\nCodewhale is built in the open with a growing community of contributors. Every\nissue report and pull request is real project work — welcome at any experience\nlevel. This is the full per-PR contributor record, grouped into release/time\nbands with the newest band first so it stays scannable. Expand any band to see\neveryone.\n\nFor the live list, see the\n[GitHub contributors page](https://github.com/codewhale-hq/CodeWhale/graphs/contributors),\n[`AUTHOR_MAP`](https://github.com/codewhale-hq/CodeWhale/blob/main/.github/AUTHOR_MAP),\nand [CHANGELOG.md](../CHANGELOG.md).\n\n<!-- contributor-wall:start -->\n## Everyone\n\n[](#everyone)\n\n421 people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts 203 of them. If your commits are missing there, add your commit email to your GitHub account and they will link.\n\n[0gl20shk0sbt36](https://github.com/0gl20shk0sbt36) · [1Git2Clone](https://github.com/1Git2Clone) · [20bytes](https://github.com/20bytes) · [47Cid](https://github.com/47Cid) · [7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) · [8sero](https://github.com/8sero) · [A-Corner](https://github.com/A-Corner) · [aboimpinto](https://github.com/aboimpinto) · [adity982](https://github.com/adity982) · [AdityaVG13](https://github.com/AdityaVG13) · [AGSaturn](https://github.com/AGSaturn) · [ai-dengwsh](https://github.com/ai-dengwsh) · [aiapienthusiast](https://github.com/aiapienthusiast) · [Aitensa](https://github.com/Aitensa) · [AiurArtanis](https://github.com/AiurArtanis) · [alitvak69](https://github.com/alitvak69) · [all-lopezg](https://github.com/all-lopezg) · [alvin1](https://github.com/alvin1) · [amuthantamil](https://github.com/amuthantamil) · [AnaheimEX](https://github.com/AnaheimEX) · [Andrea-Bruno](https://github.com/Andrea-Bruno) · [Angel-Hair](https://github.com/Angel-Hair) · [angus-guo](https://github.com/angus-guo) · [angziii](https://github.com/angziii) · [AnonymousUser443](https://github.com/AnonymousUser443) · [antarikshraya](https://github.com/antarikshraya) · [Anyexyz](https://github.com/Anyexyz) · [Apeiron0w0](https://github.com/Apeiron0w0) · [aqilaziz](https://github.com/aqilaziz) · [AresNing](https://github.com/AresNing) · [ArronAI007](https://github.com/ArronAI007) · [Artenx](https://github.com/Artenx) · [asdfg314284230](https://github.com/asdfg314284230) · [Asteria1028](https://github.com/Asteria1028) · [asto18089](https://github.com/asto18089) · [atmosuwiryo](https://github.com/atmosuwiryo) · [axobase001](https://github.com/axobase001) · [aznikline](https://github.com/aznikline) · [baendlorel](https://github.com/baendlorel) · [banqii](https://github.com/banqii) · [Beltran12138](https://github.com/Beltran12138) · [bevis-wong](https://github.com/bevis-wong) · [BH8GCJ](https://github.com/BH8GCJ) · [BigBenLabs](https://github.com/BigBenLabs) · [Biilow](https://github.com/Biilow) · [bistack](https://github.com/bistack) · [BrathonBai](https://github.com/BrathonBai) · [bruce6135](https://github.com/bruce6135) · [BryonGo](https://github.com/BryonGo) · [buiducnhat](https://github.com/buiducnhat) · [buko](https://github.com/buko) · [BX166](https://github.com/BX166) · [c020627](https://github.com/c020627) · [cacdcaecawae](https://github.com/cacdcaecawae) · [caeserchen](https://github.com/caeserchen) · [caiyilian](https://github.com/caiyilian) · [CCChisato](https://github.com/CCChisato) · [ccomma](https://github.com/ccomma) · [cenab](https://github.com/cenab) · [ChaceLyee2101](https://github.com/ChaceLyee2101) · [chatman-media](https://github.com/chatman-media) · [chennest](https://github.com/chennest) · [chinaqy110](https://github.com/chinaqy110) · [chnjames](https://github.com/chnjames) · [chuntseevolving](https://github.com/chuntseevolving) · [ci4ic4](https://github.com/ci4ic4) · [cian-lien](https://github.com/cian-lien) · [cmdcorp6534](https://github.com/cmdcorp6534) · [cmyyy](https://github.com/cmyyy) · [codepgq](https://github.com/codepgq) · [Colorful-glassblock](https://github.com/Colorful-glassblock) · [ComeFromTheMars](https://github.com/ComeFromTheMars) · [CrepuscularIRIS](https://github.com/CrepuscularIRIS) · [ct-jaryn](https://github.com/ct-jaryn) · [ctxyao](https://github.com/ctxyao) · [cy2311](https://github.com/cy2311) · [cyq1017](https://github.com/cyq1017) · [czf0718](https://github.com/czf0718) · [dajiaohuang](https://github.com/dajiaohuang) · [DarrellThomas](https://github.com/DarrellThomas) · [demian-welt](https://github.com/demian-welt) · [Dev-next-gen](https://github.com/Dev-next-gen) · [dfwqdyl-ui](https://github.com/dfwqdyl-ui) · [DI-HUO-MING-YI](https://github.com/DI-HUO-MING-YI) · [DieMoe233](https://github.com/DieMoe233) · [DingYong4223Line truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/RELEASE_CHECKLIST.md",
|
||||
@@ -357,8 +357,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/MODES.md",
|
||||
"sha256": "06bc6d00b279d7a3d0479b370893a900139e593a6a59db6b0d574e621f86363e",
|
||||
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work/Operate).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not part of\nthe `Tab` cycle.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nPress `Tab` to complete composer menus or cycle through the visible modes\nwhen the composer is empty: **Plan → Work → Operate → Plan**. `Tab` never sends\nor queues composer text; use `Enter` to send or queue it.\nPress `Shift+Tab` to cycle permission posture (Ask → Auto-Review → Full Access).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work`,\n`/mode plan`, or `/mode operate`.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `bash`, `agent`, `workflow`, `todo_write`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate**: Work at full strength, the \"ultra\" mode. Same tools and authority as Work; the difference is how they are used. Each substantive request becomes a goal (`create_goal`), the plan stays visible (`todo_write`), multi-part work runs through `workflow` and parallel `agent` workers, non-trivial changes get an independent reviewer before they are called done, long commands keep running in the background, and recurring or watch-type work is offered as an `automation` (created after approval). It keeps going until the goal is verified, blocked on you, or paused.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work | Operate |\n|:---|:---:|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated | same as Work |\n| `bash` | visible name; execution denied | approval- and policy-gated | same as Work; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same | same |\n| Paid or external-service tools | follows permission posture | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode | same trusted-path/trust policy as Work; fleet profiles never widen it |\n\nOperate changes scheduling emphasis, not authority. It neither adds a\nmode-specific tool denial nor bypasses the active approval, sandbox, shell,\nask-rule, repository-law, or managed-policy boundary. Plan remains the\nmode-specific execution boundary for shell and write-capable tools; that\nauthority difference does not require a different primitive vocabulary.\n\n### Operate loop (one screen)\n\n```text\nUser Line truncated
|
||||
"sha256": "e327b2042c9b782660cb0d6de9f4208364537ef1052a456a45332a61b1ee2468",
|
||||
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work/Operate).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not part of\nthe `Tab` cycle.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nPress `Tab` to complete composer menus or cycle through the visible modes\nwhen the composer is empty: **Plan → Work → Operate → Plan**. `Tab` never sends\nor queues composer text; use `Enter` to send or queue it.\nPress `Shift+Tab` to cycle permission posture (Ask → Auto-Review → Full Access).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work`,\n`/mode plan`, or `/mode operate`.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `bash`, `agent`, `workflow`, `todo_write`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate**: Work at full strength, the \"ultra\" mode. Same tools and authority as Work; the difference is how they are used. Each substantive request becomes a goal (`create_goal`), the plan stays visible (`todo_write`), multi-part work runs through `workflow` and parallel `agent` workers, non-trivial changes get an independent reviewer before they are called done, long commands keep running in the background, and recurring or watch-type work is offered as an `automation` (created after approval). It keeps going until the goal is verified, blocked on you, or paused.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work | Operate |\n|:---|:---:|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated | same as Work |\n| `bash` | visible name; execution denied | approval- and policy-gated | same as Work; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same | same |\n| Paid or external-service tools | follows permission posture | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode | same trusted-path/trust policy as Work; fleet profiles never widen it |\n\nOperate changes scheduling emphasis, not authority. It neither adds a\nmode-specific tool denial nor bypasses the active approval, sandbox, shell,\nask-rule, repository-law, or managed-policy boundary. Plan remains the\nmode-specific execution boundary for shell and write-capable tools; that\nauthority difference does not require a different primitive vocabulary.\n\n### Operate loop (one screen)\n\n```text\nUser Line truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/public-surface-facts.json",
|
||||
@@ -373,8 +373,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/ARCHITECTURE.md",
|
||||
"sha256": "a24f55a1a6c0f8c960ef6cee23ac63710d3c92f0397868e8a55f57b6289629df",
|
||||
"content": "# Codewhale Architecture\n\n> 阅读简体中文版:[zh_hans/ARCHITECTURE.md](zh_hans/ARCHITECTURE.md)。\n\nCodewhale Engine is the existing Rust execution runtime. The\n[Runtime API](RUNTIME_API.md) is its client interface, and\n[TypeScript mods](EXTENSIONS.md) contribute reviewed extensions.\n\nCurrent boundary note (read the workspace version from `Cargo.toml`; this\nboundary has held since v0.9.1):\n- `crates/tui` is still the live end-user runtime for the TUI, runtime API, task manager, and tool execution loop.\n- Other workspace crates are being split out incrementally, but they are not yet the sole runtime source of truth.\n- `crates/net` owns network policy/auditing, destination and DNS guards, proxy resolution, and Claude import planning. Host tool context, transport grants, redirects and response limits stay with their callers. Pure extension-host settings belong to `crates/config`.\n- `crates/notify` owns shared notification payload redaction, attention/category gates, sound policy, and local audio dispatch. TUI retains terminal sinks and native hosts retain banner submission.\n\n<img src=\"assets/architecture.webp\" alt=\"Codewhale architecture: clients reach the one turn loop, Engine::run_turn, through the CLI or the Runtime API; the engine uses crates/core to build requests, runs tools under approval and sandbox policy, routes to any provider through crates/config, and logs everything model-visible to the session store.\" width=\"1000\">\n\nThe diagram draws the Runtime API and the task manager as their own boxes; today\nboth are hosted inside `crates/tui`, as the note above says. The GPUI desktop\napp and the VS Code extension live outside this repository. The arrow from the\nengine to the session log follows the \"model-visible means logged\" contract\nrather than a traced write path.\n- `crates/tui` is being split into crates sized as agent work partitions:\n contracts, utilities, services, providers, runtime, leaf feature crates,\n TUI views and hosts, in the order `docs/design/TUI_DECONSTRUCTION.md`\n records. The TUI stays the only crate that writes to the terminal. Until a\n module has moved, its path under `crates/tui/src` is still where it lives.\n- The LSP subsystem (`crates/tui/src/lsp/`) is fully wired into the engine's\n post-tool-execution path (`core/engine/lsp_hooks.rs`), providing inline\n diagnostics after `File` write, edit, and patch actions.\n- The swarm agent system was removed in v0.8.5. The active sub-agent surface is\n the single `agent` tool; persistent RLM sessions are available through the\n deferred `rlm` action family.\n No model-visible swarm tool remains in the active codebase.\n\n## High-Level Overview\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ User Interface │\n│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │\n│ │ TUI (ratatui) │ │ One-shot Mode │ │ Config/CLI │ │\n│ └────────┬────────┘ └────────┬────────┘ └────────┬───────┘ │\n└───────────┼─────────────────────┼────────────────────┼──────────┘\n │ │ │\n ▼ ▼ ▼\n┌─────────────────────────────────────────────────────────────────┐\n│ Core Engine │\n│ ┌─────────────────────────────────────────────────────────┐ │\n│ │ Agent Loop (core/engine.rs) │ │\n│ │ ┌─────────┐ ┌─────────────┐ ┌──────────────────────┐ │ │\n│ │ │ Session │ │ Turn Mgmt │ │ Tool Orchestration │ │ │\n│ │ └─────────┘ └─────────────┘ └──────────────────────┘ │ │\n│ └─────────────────────────────────────────────────────────┘ │\n└─────────Line truncated
|
||||
"sha256": "ba6a71173f089fbc0e1ff467ca1f0f67e20a0df0a9f1961596d89102acec926d",
|
||||
"content": "# Codewhale Architecture\n\n> 阅读简体中文版:[zh_hans/ARCHITECTURE.md](zh_hans/ARCHITECTURE.md)。\n\nCodewhale Engine is the existing Rust execution runtime. The\n[Runtime API](RUNTIME_API.md) is its client interface, and\n[TypeScript mods](EXTENSIONS.md) contribute reviewed extensions.\n\nCurrent boundary note (read the workspace version from `Cargo.toml`; this\nboundary has held since v0.9.1):\n- `crates/tui` is still the live end-user runtime for the TUI, runtime API, task manager, and tool execution loop.\n- Other workspace crates are being split out incrementally, but they are not yet the sole runtime source of truth.\n- `crates/net` owns network policy/auditing, destination and DNS guards, proxy resolution, and Claude import planning. Host tool context, transport grants, redirects and response limits stay with their callers. Pure extension-host settings belong to `crates/config`.\n- `crates/notify` owns shared notification payload redaction, attention/category gates, sound policy, and local audio dispatch. TUI retains terminal sinks and native hosts retain banner submission.\n- `crates/util` owns shared string/path/atomic-write helpers, confined workspace file access, link and file-identity checks, bundle file readers, the `.git` name rule and the exclusive file lock.\n- `crates/dependencies` owns external-tool resolution and bounded subprocess probes (git, python, node, bun); `crates/snapshot` the pre/post-turn side-repo snapshots; `crates/lsp` the language-server client; `crates/repl` the Python REPL runtime and RLM context files.\n- `crates/skill-install` owns the community-skill installer (GitHub and tarball sources, the curated registry, bounded extraction, install and trust markers), the package content digest and the shared `SKILL.md` frontmatter reader. Its SSRF DNS pin and shortened timeouts are relaxed only under its dev-dependency-only `test-support` feature.\n\n<img src=\"assets/architecture.webp\" alt=\"Codewhale architecture: clients reach the one turn loop, Engine::run_turn, through the CLI or the Runtime API; the engine uses crates/core to build requests, runs tools under approval and sandbox policy, routes to any provider through crates/config, and logs everything model-visible to the session store.\" width=\"1000\">\n\nThe diagram draws the Runtime API and the task manager as their own boxes; today\nboth are hosted inside `crates/tui`, as the note above says. The GPUI desktop\napp and the VS Code extension live outside this repository. The arrow from the\nengine to the session log follows the \"model-visible means logged\" contract\nrather than a traced write path.\n- `crates/tui` is being split into crates sized as agent work partitions:\n contracts, utilities, services, providers, runtime, leaf feature crates,\n TUI views and hosts, in the order `docs/design/TUI_DECONSTRUCTION.md`\n records. The TUI stays the only crate that writes to the terminal. Until a\n module has moved, its path under `crates/tui/src` is still where it lives.\n- The LSP subsystem (`crates/lsp/src/`) is fully wired into the engine's\n post-tool-execution path (`core/engine/lsp_hooks.rs`), providing inline\n diagnostics after `File` write, edit, and patch actions.\n- The swarm agent system was removed in v0.8.5. The active sub-agent surface is\n the single `agent` tool; persistent RLM sessions are available through the\n deferred `rlm` action family.\n No model-visible swarm tool remains in the active codebase.\n\n## High-Level Overview\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ User Interface │\n│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │\n│ │ TUI (ratatui) │ │ One-shot Mode │ │ Config/CLI │ │\n│ └────────┬────────┘ └────────┬────────┘ └────────┬───────┘ │\n└───────────┼─────────────────────┼────────────────────┼──────────┘\n │ │ │\n ▼ ▼ ▼\n┌─────────────────────────────────────────────────────────────────┐\n│ Core Engine │\n│ ┌─────────────────────────────────────Line truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -394,8 +394,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/LSP_PHP_CUSTOM.md",
|
||||
"sha256": "2dc48d7688a92844d6a5adf682e457b9bcb30f54c64c838af254d35feb4b659f",
|
||||
"content": "# LSP: PHP Support & Custom Language Server Extension\n\n> 阅读简体中文版:[zh_hans/LSP_PHP_CUSTOM.md](zh_hans/LSP_PHP_CUSTOM.md)\n>\n> v0.8.65+ | `codex/lsp-php-custom-servers`\n\n## Overview\n\nThis feature adds **PHP** to the built-in LSP language registry and introduces a\n`[lsp.custom]` config section so users can register arbitrary LSP servers by\nfile extension — covering languages absent from the built-in `Language` enum\n(Ruby, C#, Swift, Lua, etc.).\n\n## Changes\n\n### 1. PHP built-in support\n\n- `Language::Php` variant added to the enum in `crates/tui/src/lsp/registry.rs`\n- `.php` files are detected and routed to `intelephense --stdio` by default\n- User can override via `[lsp.servers].php`\n\n### 2. Custom LSP server extension\n\nNew struct `CustomLspDef` (defined in `crates/tui/src/lsp/mod.rs` and mirrored\nin `crates/config/src/lib.rs`):\n\n```rust\npub struct CustomLspDef {\n pub language_id: String, // LSP languageId for textDocument/didOpen\n pub command: String, // executable to spawn\n pub args: Vec<String>, // arguments (default empty)\n}\n```\n\nNew config field `LspConfig.custom: HashMap<String, CustomLspDef>` — keyed by\nfile extension (without dot), e.g. `\"rb\"`, `\"cs\"`, `\"swift\"`.\n\nIn `LspManager::diagnostics_for`, when the built-in registry returns\n`Language::Other`, the manager checks the user's custom table before giving up.\nCustom servers get their own lazy-spawn transport map and once-per-extension\nmissing-binary warnings (no log spam).\n\n### 3. Transport generalization\n\n`StdioLspTransport::spawn` now accepts `&str language_id` instead of\n`Language`, so both built-in and custom servers share the same transport\nimplementation. The old `Language` import is removed from `client.rs`.\n\n### 4. Polling pipeline extracted\n\n`poll_diagnostics` is a new private method that both built-in and custom\ndiagnostics paths share — eliminating duplicated call/wait/filter/sort/truncate\nlogic.\n\n## Configuration\n\n### Built-in PHP (enabled by default if `intelephense` is on PATH)\n\n```toml\n# No config needed — PHP .php files are detected automatically.\n# Override the server if desired:\n[lsp.servers]\nphp = [\"phpactor\", \"language-server\"]\n```\n\n### Custom language servers\n\n```toml\n[lsp.custom.rb]\ncommand = \"ruby-lsp\"\nargs = [\"--stdio\"]\nlanguage_id = \"ruby\"\n\n[lsp.custom.cs]\ncommand = \"csharp-ls\"\nlanguage_id = \"csharp\"\n\n[lsp.custom.swift]\ncommand = \"sourcekit-lsp\"\nlanguage_id = \"swift\"\n```\n\nKeys are file extensions (no leading dot). The `args` field defaults to empty.\n`language_id` must match what the LSP server expects in `textDocument/didOpen`.\n\n## Architecture\n\n```\nedit_file / write_file / apply_patch success\n │\n ▼\n LspManager.diagnostics_for(file)\n │\n ├── custom_for_extension(file) ── found? ──► transport_for_custom(ext, def)\n │ │\n ├── detect_language(file) ── Other? ──► return None (skip)\n │\n └── transport_for(lang)\n │\n ▼\n poll_diagnostics(file, text, transport)\n │\n ▼\n DiagnosticBlock → injected into session message stream\n```\n\n## Verification\n\n```\ncargo test -p codewhale-tui --lib lsp::\n# 32 tests passed (3 new: detects_php_extension, language_ids_for_php,\n# server_for_php_is_intelephense)\ncargo clippy -p codewhale-tui --lib\n# lsp module: zero new warnings\n```\n\n## Files touched\n\n| File | Change |\n|------|--------|\n| `crates/tui/src/lsp/registry.rs` | +Php variant, detection, server mapping, tests |\n| `crates/tui/src/lsp/mod.rs` | +CustomLspDef, LspConfig.custom, LspManager custom fallback |\n| `crates/tui/src/lsp/client.rs` | spawn accepts &str language_id |\n| `crates/tui/src/config.rs` | LspConfigToml.custom + into_runtime |\n| `crates/config/src/lib.rs` | LspConfigToml.custom + CustomLspDef |\n| `config.example.toml` | PHP docs + custom extension examples |\n"
|
||||
"sha256": "be77d5f68a990d452cc99523b4d9982267c18a7d1b30766d31e5385e0bfed453",
|
||||
"content": "# LSP: PHP Support & Custom Language Server Extension\n\n> 阅读简体中文版:[zh_hans/LSP_PHP_CUSTOM.md](zh_hans/LSP_PHP_CUSTOM.md)\n>\n> v0.8.65+ | `codex/lsp-php-custom-servers`\n\n## Overview\n\nThis feature adds **PHP** to the built-in LSP language registry and introduces a\n`[lsp.custom]` config section so users can register arbitrary LSP servers by\nfile extension — covering languages absent from the built-in `Language` enum\n(Ruby, C#, Swift, Lua, etc.).\n\n## Changes\n\n### 1. PHP built-in support\n\n- `Language::Php` variant added to the enum in `crates/lsp/src/registry.rs`\n- `.php` files are detected and routed to `intelephense --stdio` by default\n- User can override via `[lsp.servers].php`\n\n### 2. Custom LSP server extension\n\nNew struct `CustomLspDef` (defined in `crates/lsp/src/lib.rs` and mirrored\nin `crates/config/src/lib.rs`):\n\n```rust\npub struct CustomLspDef {\n pub language_id: String, // LSP languageId for textDocument/didOpen\n pub command: String, // executable to spawn\n pub args: Vec<String>, // arguments (default empty)\n}\n```\n\nNew config field `LspConfig.custom: HashMap<String, CustomLspDef>` — keyed by\nfile extension (without dot), e.g. `\"rb\"`, `\"cs\"`, `\"swift\"`.\n\nIn `LspManager::diagnostics_for`, when the built-in registry returns\n`Language::Other`, the manager checks the user's custom table before giving up.\nCustom servers get their own lazy-spawn transport map and once-per-extension\nmissing-binary warnings (no log spam).\n\n### 3. Transport generalization\n\n`StdioLspTransport::spawn` now accepts `&str language_id` instead of\n`Language`, so both built-in and custom servers share the same transport\nimplementation. The old `Language` import is removed from `client.rs`.\n\n### 4. Polling pipeline extracted\n\n`poll_diagnostics` is a new private method that both built-in and custom\ndiagnostics paths share — eliminating duplicated call/wait/filter/sort/truncate\nlogic.\n\n## Configuration\n\n### Built-in PHP (enabled by default if `intelephense` is on PATH)\n\n```toml\n# No config needed — PHP .php files are detected automatically.\n# Override the server if desired:\n[lsp.servers]\nphp = [\"phpactor\", \"language-server\"]\n```\n\n### Custom language servers\n\n```toml\n[lsp.custom.rb]\ncommand = \"ruby-lsp\"\nargs = [\"--stdio\"]\nlanguage_id = \"ruby\"\n\n[lsp.custom.cs]\ncommand = \"csharp-ls\"\nlanguage_id = \"csharp\"\n\n[lsp.custom.swift]\ncommand = \"sourcekit-lsp\"\nlanguage_id = \"swift\"\n```\n\nKeys are file extensions (no leading dot). The `args` field defaults to empty.\n`language_id` must match what the LSP server expects in `textDocument/didOpen`.\n\n## Architecture\n\n```\nedit_file / write_file / apply_patch success\n │\n ▼\n LspManager.diagnostics_for(file)\n │\n ├── custom_for_extension(file) ── found? ──► transport_for_custom(ext, def)\n │ │\n ├── detect_language(file) ── Other? ──► return None (skip)\n │\n └── transport_for(lang)\n │\n ▼\n poll_diagnostics(file, text, transport)\n │\n ▼\n DiagnosticBlock → injected into session message stream\n```\n\n## Verification\n\n```\ncargo test -p codewhale-tui --lib lsp::\n# 32 tests passed (3 new: detects_php_extension, language_ids_for_php,\n# server_for_php_is_intelephense)\ncargo clippy -p codewhale-tui --lib\n# lsp module: zero new warnings\n```\n\n## Files touched\n\n| File | Change |\n|------|--------|\n| `crates/lsp/src/registry.rs` | +Php variant, detection, server mapping, tests |\n| `crates/lsp/src/lib.rs` | +CustomLspDef, LspConfig.custom, LspManager custom fallback |\n| `crates/lsp/src/client.rs` | spawn accepts &str language_id |\n| `crates/tui/src/config.rs` | LspConfigToml.custom + into_runtime |\n| `crates/config/src/lib.rs` | LspConfigToml.custom + CustomLspDef |\n| `config.example.toml` | PHP docs + custom extension examples |\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
"format": "codewhale.features.v1",
|
||||
"source": {
|
||||
"path": "docs/features.toml",
|
||||
"sha256": "0a0148946fe92a675a396ffd207fbab3debd1bcdec70c8e3227baf9340b30ffa"
|
||||
"sha256": "c1673c4a1d668f3f9fe82c09ea016b9e3bf1f71a4f0c1076253af5db06c80a79"
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"id": "unix-uninstall",
|
||||
"name": "Receipt-owned Unix uninstall",
|
||||
"summary": "Preview or remove this receipt-owned Unix release installation while preserving user data and other installations.",
|
||||
"name": "Receipt-owned uninstall",
|
||||
"summary": "Remove a receipt-owned release install or print its package manager's command; data kept unless --purge-data.",
|
||||
"status": "experimental",
|
||||
"surfaces": {
|
||||
"tui": "preview"
|
||||
@@ -454,6 +454,32 @@
|
||||
"docs": "docs/CONFIGURATION.md#goal-loop-goal",
|
||||
"registryLine": 371
|
||||
},
|
||||
{
|
||||
"id": "thread-scheduled-turns",
|
||||
"name": "Reminders into the same thread",
|
||||
"summary": "send_later, automations and /v1/triggers can deliver a scheduled message as a new turn in an existing thread.",
|
||||
"status": "experimental",
|
||||
"surfaces": {
|
||||
"api": "preview"
|
||||
},
|
||||
"since": "unreleased",
|
||||
"flag": null,
|
||||
"docs": "docs/RUNTIME_API.md",
|
||||
"registryLine": 380
|
||||
},
|
||||
{
|
||||
"id": "task-suggestions",
|
||||
"name": "Suggested tasks",
|
||||
"summary": "The tasks tool's suggest action offers the person a task as a Start/Hide card; it starts nothing and needs no approval.",
|
||||
"status": "experimental",
|
||||
"surfaces": {
|
||||
"desktop": "preview"
|
||||
},
|
||||
"since": "unreleased",
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 390
|
||||
},
|
||||
{
|
||||
"id": "todos",
|
||||
"name": "Todo list",
|
||||
@@ -466,7 +492,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/TOOL_SURFACE.md",
|
||||
"registryLine": 380
|
||||
"registryLine": 399
|
||||
},
|
||||
{
|
||||
"id": "composer-queue-steer",
|
||||
@@ -480,7 +506,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/KEYBINDINGS.md",
|
||||
"registryLine": 389
|
||||
"registryLine": 408
|
||||
},
|
||||
{
|
||||
"id": "receipts",
|
||||
@@ -494,7 +520,7 @@
|
||||
"since": "0.10.1",
|
||||
"flag": null,
|
||||
"docs": "docs/RECEIPTS.md",
|
||||
"registryLine": 398
|
||||
"registryLine": 417
|
||||
},
|
||||
{
|
||||
"id": "preview-request",
|
||||
@@ -507,7 +533,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/PREVIEW_REQUEST.md",
|
||||
"registryLine": 408
|
||||
"registryLine": 427
|
||||
},
|
||||
{
|
||||
"id": "read-media",
|
||||
@@ -520,7 +546,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/READ_MEDIA.md",
|
||||
"registryLine": 417
|
||||
"registryLine": 436
|
||||
},
|
||||
{
|
||||
"id": "memory",
|
||||
@@ -534,7 +560,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/MEMORY.md",
|
||||
"registryLine": 426
|
||||
"registryLine": 445
|
||||
},
|
||||
{
|
||||
"id": "skills",
|
||||
@@ -548,7 +574,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/SKILLS.md",
|
||||
"registryLine": 437
|
||||
"registryLine": 456
|
||||
},
|
||||
{
|
||||
"id": "plugins",
|
||||
@@ -562,7 +588,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/PLUGINS.md",
|
||||
"registryLine": 446
|
||||
"registryLine": 465
|
||||
},
|
||||
{
|
||||
"id": "computer-use",
|
||||
@@ -576,7 +602,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/PLUGIN_BUNDLES.md",
|
||||
"registryLine": 455
|
||||
"registryLine": 474
|
||||
},
|
||||
{
|
||||
"id": "script-tools",
|
||||
@@ -589,7 +615,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/CONFIGURATION.md#script-tools-and-overrides",
|
||||
"registryLine": 464
|
||||
"registryLine": 483
|
||||
},
|
||||
{
|
||||
"id": "claude-plugin-compat",
|
||||
@@ -602,7 +628,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/CLAUDE_PLUGIN_COMPAT.md",
|
||||
"registryLine": 473
|
||||
"registryLine": 492
|
||||
},
|
||||
{
|
||||
"id": "claude-settings-import",
|
||||
@@ -615,7 +641,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 482
|
||||
"registryLine": 501
|
||||
},
|
||||
{
|
||||
"id": "hooks",
|
||||
@@ -629,7 +655,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/HOOKS.md",
|
||||
"registryLine": 490
|
||||
"registryLine": 509
|
||||
},
|
||||
{
|
||||
"id": "lsp",
|
||||
@@ -643,7 +669,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/LSP_PHP_CUSTOM.md",
|
||||
"registryLine": 499
|
||||
"registryLine": 518
|
||||
},
|
||||
{
|
||||
"id": "runtime-api",
|
||||
@@ -656,7 +682,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/RUNTIME_API.md",
|
||||
"registryLine": 508
|
||||
"registryLine": 527
|
||||
},
|
||||
{
|
||||
"id": "compatibility-tool-free-turns",
|
||||
@@ -669,7 +695,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": "docs/RUNTIME_API.md#compatibility-execution-limits",
|
||||
"registryLine": 517
|
||||
"registryLine": 536
|
||||
},
|
||||
{
|
||||
"id": "runtime-provider-keys",
|
||||
@@ -682,7 +708,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": "docs/RUNTIME_API.md#put-v1providersidkey--write-only-credential",
|
||||
"registryLine": 527
|
||||
"registryLine": 546
|
||||
},
|
||||
{
|
||||
"id": "headless-exec",
|
||||
@@ -695,7 +721,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/RUNTIME_API.md",
|
||||
"registryLine": 537
|
||||
"registryLine": 556
|
||||
},
|
||||
{
|
||||
"id": "github-review",
|
||||
@@ -708,7 +734,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/GITHUB_APP.md",
|
||||
"registryLine": 546
|
||||
"registryLine": 565
|
||||
},
|
||||
{
|
||||
"id": "web-client",
|
||||
@@ -721,7 +747,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/WEB.md",
|
||||
"registryLine": 557
|
||||
"registryLine": 576
|
||||
},
|
||||
{
|
||||
"id": "keybindings",
|
||||
@@ -734,7 +760,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/KEYBINDINGS.md",
|
||||
"registryLine": 566
|
||||
"registryLine": 585
|
||||
},
|
||||
{
|
||||
"id": "settings",
|
||||
@@ -748,7 +774,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/CONFIGURATION.md",
|
||||
"registryLine": 575
|
||||
"registryLine": 594
|
||||
},
|
||||
{
|
||||
"id": "whats-new-notice",
|
||||
@@ -761,7 +787,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": "docs/CONFIGURATION.md#whats-new-after-an-update",
|
||||
"registryLine": 584
|
||||
"registryLine": 603
|
||||
},
|
||||
{
|
||||
"id": "themes",
|
||||
@@ -774,7 +800,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/schemas/user-theme.schema.json",
|
||||
"registryLine": 594
|
||||
"registryLine": 613
|
||||
},
|
||||
{
|
||||
"id": "chat-color",
|
||||
@@ -787,7 +813,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": "docs/GUIDE.md",
|
||||
"registryLine": 603
|
||||
"registryLine": 622
|
||||
},
|
||||
{
|
||||
"id": "localization",
|
||||
@@ -800,7 +826,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/LOCALIZATION.md",
|
||||
"registryLine": 613
|
||||
"registryLine": 632
|
||||
},
|
||||
{
|
||||
"id": "voice",
|
||||
@@ -814,7 +840,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 622
|
||||
"registryLine": 641
|
||||
},
|
||||
{
|
||||
"id": "pet",
|
||||
@@ -828,7 +854,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/PET_MODE.md",
|
||||
"registryLine": 630
|
||||
"registryLine": 649
|
||||
},
|
||||
{
|
||||
"id": "overview",
|
||||
@@ -841,7 +867,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/PROVIDERS.md",
|
||||
"registryLine": 639
|
||||
"registryLine": 658
|
||||
},
|
||||
{
|
||||
"id": "account-login",
|
||||
@@ -854,7 +880,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 648
|
||||
"registryLine": 667
|
||||
},
|
||||
{
|
||||
"id": "account-computer-files",
|
||||
@@ -867,7 +893,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 656
|
||||
"registryLine": 675
|
||||
},
|
||||
{
|
||||
"id": "context-anchors",
|
||||
@@ -880,7 +906,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 665
|
||||
"registryLine": 684
|
||||
},
|
||||
{
|
||||
"id": "window-pin",
|
||||
@@ -893,7 +919,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 673
|
||||
"registryLine": 692
|
||||
},
|
||||
{
|
||||
"id": "structured-copy",
|
||||
@@ -906,7 +932,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 681
|
||||
"registryLine": 700
|
||||
},
|
||||
{
|
||||
"id": "live-thinking",
|
||||
@@ -919,7 +945,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 689
|
||||
"registryLine": 708
|
||||
},
|
||||
{
|
||||
"id": "workbar",
|
||||
@@ -932,7 +958,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 697
|
||||
"registryLine": 716
|
||||
},
|
||||
{
|
||||
"id": "terminal-dock",
|
||||
@@ -945,7 +971,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 705
|
||||
"registryLine": 724
|
||||
},
|
||||
{
|
||||
"id": "telemetry",
|
||||
@@ -958,7 +984,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/TELEMETRY.md",
|
||||
"registryLine": 716
|
||||
"registryLine": 735
|
||||
},
|
||||
{
|
||||
"id": "docker",
|
||||
@@ -971,7 +997,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/DOCKER.md",
|
||||
"registryLine": 725
|
||||
"registryLine": 744
|
||||
},
|
||||
{
|
||||
"id": "termux",
|
||||
@@ -984,7 +1010,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/TERMUX.md",
|
||||
"registryLine": 734
|
||||
"registryLine": 753
|
||||
},
|
||||
{
|
||||
"id": "chatgpt-plan-sign-in",
|
||||
@@ -999,7 +1025,7 @@
|
||||
"since": "0.10.1",
|
||||
"flag": null,
|
||||
"docs": "docs/PROVIDERS.md#sign-in-with-chatgpt",
|
||||
"registryLine": 743
|
||||
"registryLine": 762
|
||||
},
|
||||
{
|
||||
"id": "claude-plan-sign-in",
|
||||
@@ -1014,7 +1040,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": "docs/PROVIDERS.md#sign-in-with-claude",
|
||||
"registryLine": 753
|
||||
"registryLine": 772
|
||||
},
|
||||
{
|
||||
"id": "plugin-oauth-providers",
|
||||
@@ -1029,7 +1055,7 @@
|
||||
"since": "0.10.1",
|
||||
"flag": null,
|
||||
"docs": "docs/PLUGIN_PROVIDERS.md",
|
||||
"registryLine": 763
|
||||
"registryLine": 782
|
||||
},
|
||||
{
|
||||
"id": "orcarouter-sign-in",
|
||||
@@ -1044,7 +1070,7 @@
|
||||
"since": "0.10.1",
|
||||
"flag": null,
|
||||
"docs": "docs/PROVIDERS.md",
|
||||
"registryLine": 773
|
||||
"registryLine": 792
|
||||
},
|
||||
{
|
||||
"id": "merch-interest",
|
||||
@@ -1059,7 +1085,7 @@
|
||||
"since": "unreleased",
|
||||
"flag": null,
|
||||
"docs": "web/lib/merch/README.md",
|
||||
"registryLine": 784
|
||||
"registryLine": 803
|
||||
},
|
||||
{
|
||||
"id": "profile-constitution",
|
||||
@@ -1073,7 +1099,7 @@
|
||||
"since": "0.10.1",
|
||||
"flag": null,
|
||||
"docs": "docs/RUNTIME_API.md#profile-constitution",
|
||||
"registryLine": 794
|
||||
"registryLine": 813
|
||||
},
|
||||
{
|
||||
"id": "plugin-shell-install",
|
||||
@@ -1086,7 +1112,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": "docs/PLUGINS.md",
|
||||
"registryLine": 804
|
||||
"registryLine": 823
|
||||
},
|
||||
{
|
||||
"id": "herdr-agent-state",
|
||||
@@ -1099,7 +1125,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 814
|
||||
"registryLine": 833
|
||||
},
|
||||
{
|
||||
"id": "instruction-budgets",
|
||||
@@ -1112,7 +1138,7 @@
|
||||
"since": "unreleased",
|
||||
"flag": null,
|
||||
"docs": "docs/CONFIGURATION.md#instruction-budget-instruction_budget-6526",
|
||||
"registryLine": 823
|
||||
"registryLine": 842
|
||||
},
|
||||
{
|
||||
"id": "github-action-review",
|
||||
@@ -1125,7 +1151,7 @@
|
||||
"since": "unreleased",
|
||||
"flag": null,
|
||||
"docs": "docs/GITHUB_ACTION.md",
|
||||
"registryLine": 833
|
||||
"registryLine": 852
|
||||
},
|
||||
{
|
||||
"id": "telegram-agent",
|
||||
@@ -1138,7 +1164,7 @@
|
||||
"since": "unreleased",
|
||||
"flag": null,
|
||||
"docs": "integrations/telegram-bridge/README.md",
|
||||
"registryLine": 843
|
||||
"registryLine": 862
|
||||
},
|
||||
{
|
||||
"id": "dingtalk-agent",
|
||||
@@ -1151,7 +1177,7 @@
|
||||
"since": "unreleased",
|
||||
"flag": null,
|
||||
"docs": "integrations/dingtalk-bridge/README.md",
|
||||
"registryLine": 853
|
||||
"registryLine": 872
|
||||
}
|
||||
]
|
||||
}
|
||||
+47
-117
@@ -1,129 +1,59 @@
|
||||
# Site colors and the Moda brand kit
|
||||
# Site colors and the design tokens
|
||||
|
||||
Decision (founder, 2026-10-11): the Moda brand kit "Codewhale"
|
||||
(`bk_3S5VAJCR69901RXWE5S5DFR3B3`) is the source of truth for color. The site's
|
||||
role tokens in `app/styles/tokens-roles.css` paint with `--kit-*` (the kit's
|
||||
original 25 colors) and nothing else. Geometry (radii, focus, spacing) still
|
||||
comes from the generated `--gpui-*` tokens.
|
||||
Decision (founder, 2026-10-27): the website uses the same design system as the
|
||||
desktop app. `codewhale-design` `tokens.json` (2.3.0, vendored at
|
||||
`vendor/codewhale-design`) is the source of truth for color, type roles,
|
||||
spacing, radius, motion and elevation. This supersedes the 2026-10-11 decision
|
||||
that made the Moda brand kit "Codewhale" the site's color authority.
|
||||
|
||||
## What was not changed, and why
|
||||
## How the values reach a page
|
||||
|
||||
`app/tokens.css` is generated by `scripts/export-design-tokens.py` from
|
||||
`crates/palette/src/rgb.rs` (the terminal palettes) and
|
||||
`vendor/codewhale-design/tokens.json`. Its 131 off-kit colors (`--whale-*`,
|
||||
`--light-*`, `--shoreline-*`) are the terminal's palettes. No page, component
|
||||
or stylesheet reads them (grep of `app`, `components`, `lib`); they only feed
|
||||
the palette contract tests. They are not site colors and are left to the
|
||||
palette crate. The `--gpui-*` colors in that file are no longer read by any
|
||||
role; they remain generated for parity checks.
|
||||
1. `scripts/export-design-tokens.py` reads the vendored `tokens.json` and
|
||||
writes `app/tokens.css`: `--gpui-light-*` / `--gpui-dark-*` colors,
|
||||
`--gpui-type-*`, `--gpui-motion-*`, `--gpui-easing-*`,
|
||||
`--gpui-elevation-*`, `--gpui-radius-*`, `--gpui-spacing-*`,
|
||||
`--gpui-breakpoint-*` and `--gpui-ombre`. `npm run check:tokens` fails when
|
||||
it is stale.
|
||||
2. `app/styles/tokens-roles.css` names the roles (`--bg`, `--text`,
|
||||
`--accent`, `--brand-fill`, `--text-ui`, `--dur-state` and so on). Every
|
||||
role is a `var(--gpui-*)`, never a literal.
|
||||
3. Pages and components paint only with roles.
|
||||
|
||||
The `#0a1e33` / `#f6f2e8` pair in `primitives.css` is the captured terminal
|
||||
frame's real colors and stays as captured.
|
||||
To change a color, change `tokens.json` in `codewhale-design`, bump its
|
||||
version, run `generate.py`, sync it here with `scripts/sync-to.sh`, then run
|
||||
`scripts/export-design-tokens.py`.
|
||||
|
||||
## Role mapping (tokens-roles.css)
|
||||
## Role mapping
|
||||
|
||||
| Role | Old | Kit color (new) |
|
||||
| Role | Light | Dark (pinned) and the stage |
|
||||
| --- | --- | --- |
|
||||
| light bg | gpui #faf8f5 | Warm Paper #FAF9F5 |
|
||||
| light surface (recessed) | gpui sidebar #f0ede8 | Seaglass #EDF1EF |
|
||||
| light panel (raised) | #ffffff | White #FFFFFF |
|
||||
| light text | #28292b | Ink #263844 |
|
||||
| light muted | #535451 | Slate #495A63 |
|
||||
| light line | #d9d5cf | Tideline #D2DEDD |
|
||||
| light line-strong | #807c76 | Slate #495A63 (see gap 1) |
|
||||
| light accent / ring | #245bc7 | Signal Blue #245BC7 |
|
||||
| light on-accent | #fbf5ee | White #FFFFFF |
|
||||
| light hover / selected | #e8e5e0 / #dfdcd6 | Seaglass / Shallows #DCEFEB |
|
||||
| light live | #3a6352 | Kelp Deep #396554 |
|
||||
| light attention | #86520d | Amber #84530D |
|
||||
| light danger | #9e3f36 | Rust #9E3F36 |
|
||||
| dark bg (ocean-floor) | #061431 | Deep Navy #142638 |
|
||||
| dark surface (ocean-abyss) | #030a1c | Abyss #0F1E2D |
|
||||
| dark panel (ocean-raised) | #0c2152 | Ocean Depth #173B50 |
|
||||
| dark hover | #11295e | Night Surface #20374E |
|
||||
| dark selected | #16316b | Night Border #31495E |
|
||||
| dark line | #1f3a78 | Night Border #31495E |
|
||||
| dark line-strong | #6f86b8 | Mist #BBC9D7 (see gap 1) |
|
||||
| ocean-sky (dark ground top) | #0a1d48 | Mark Navy #142352 |
|
||||
| dark text / muted | #efeeeb / #c2c1bd | Foam #E6EEF7 / Mist #BBC9D7 |
|
||||
| dark accent / ring | #90b9ff | Bioluminescence #8FBDFF |
|
||||
| dark on-accent | #15243e | Abyss #0F1E2D |
|
||||
| dark live | #9ec7b2 | Kelp #9ECBBD |
|
||||
| dark attention | #e8b077 | Lantern #EAB982 |
|
||||
| dark danger | #e39a90 | Coral #EEA59D |
|
||||
| brand-fill (primary button) | #1877c9 -> #0b48bb | Signal Blue -> Whale Blue |
|
||||
| brand-fill-hover | #1670c2 -> #0a3fa6 | Whale Blue -> Mark Navy |
|
||||
| sea ramp | #0a3a94, #072a6e, #061431 | Whale Blue, Mark Navy, Deep Navy |
|
||||
| tide-aqua | #d3ebf7 | Shallows #DCEFEB |
|
||||
| dark ombre text | #7cc2f2 -> #1e8fd8 | Bioluminescence -> Current Blue |
|
||||
| horizon light | rgb(144 185 255) | Bioluminescence rgb(143 189 255) |
|
||||
| diff add / del (light) | rgb(104 152 119) / rgb(189 119 116) | Kelp Deep / Rust, same alpha |
|
||||
| diff add / del (dark) | rgb(80 170 140) / rgb(227 154 144) | Kelp / Coral, same alpha |
|
||||
| OG image gradient | #0a1d48, #061431, #0b48bb | Mark Navy, Deep Navy, Whale Blue |
|
||||
| OG phrase | #efeeeb | Foam |
|
||||
| manifest theme/background | #061431 | Deep Navy #142638 |
|
||||
| merch caption on navy | #b3c5e8 | Mist #BBC9D7 |
|
||||
| whale belly in SVG and icon PNGs | #F4F8FF | White #FFFFFF |
|
||||
| `--bg` | `background` #faf8f5 | `background` #202123 (stage: `ocean_depth` #202123) |
|
||||
| `--surface` (recessed) | `sidebar` #f0ede8 | `sidebar` #191a1c |
|
||||
| `--panel` (raised) | `surface` #ffffff | `surface` #2a2b2e |
|
||||
| `--text` | `foreground` #28292b | `foreground` #efeeeb |
|
||||
| `--muted` | `muted_foreground` #4f504d | `muted_foreground` #b8b8b4 |
|
||||
| `--line` / `--line-strong` | `border` / `border_strong` | `border` / `border_strong` |
|
||||
| `--accent`, `--ring` (links, focus, selection) | `accent` #425c82 | `accent` #a9c0de |
|
||||
| `--brand-fill` / `--on-brand` (filled actions) | `primary` #2e2f31 / `primary_foreground` | `primary` #e6e4df / `primary_foreground` |
|
||||
| `--hover` / `--selected` | `hover` / `selected` | `hover` / `selected` |
|
||||
| `--live` / `--attention` / `--danger` | token roles | token roles |
|
||||
| `--diff-add` / `--diff-del` | `diff_add` / `diff_remove` | `diff_add` / `diff_remove` |
|
||||
|
||||
The mono mark's `#000000` is the kit's "Mark - mono black" logo and stays.
|
||||
The logo ombre (`#1e8fd8` → `#0b48bb`, `--ombre`, `--brand-light`,
|
||||
`--brand-deep`) is used only on the whale and the mark.
|
||||
|
||||
## Contrast (WCAG 2.x, computed from the kit hexes)
|
||||
The footer and the home install band (`.stage`, `.site-footer`) are painted
|
||||
with the dark set in both appearances.
|
||||
|
||||
Every text ink on every ground (bg, surface, panel, hover, selected):
|
||||
## Not from tokens
|
||||
|
||||
| Scheme | Lowest text ratio | Where |
|
||||
| --- | --- | --- |
|
||||
| light | 5.18 | Signal Blue on Shallows |
|
||||
| dark and stage | 4.68 | Coral on Night Border |
|
||||
- `--whale-*`, `--light-*` and `--shoreline-*` in `app/tokens.css` are the
|
||||
terminal's palettes from `crates/palette/src/rgb.rs`. No page reads them.
|
||||
- `#0a1e33` / `#f6f2e8` in `primitives.css` are the captured terminal frame's
|
||||
real colors and stay as captured.
|
||||
- The fluid editorial sizes (`--text-section`, `--text-display`,
|
||||
`--text-display-lg`) are the website's own layout steps above the token
|
||||
`display` role.
|
||||
|
||||
Other pairs: light line-strong 6.81 on Warm Paper, 7.18 on White; dark
|
||||
line-strong 9.13 on Deep Navy, 7.00 on Ocean Depth; white on the primary fill
|
||||
6.18 (Signal Blue) and 7.90 (Whale Blue); on-accent 6.18 light, 8.76 dark;
|
||||
Foam on the sea ramp 6.75 / 12.89 / 13.15; Mist on the sea ramp 4.68 / 8.94 /
|
||||
9.13 (the lowest is Mist on Whale Blue). `lib/gpui-role-tokens.test.ts` asserts
|
||||
these floors in CI.
|
||||
|
||||
Non-text edges: the kit's hairlines are faint by design (Tideline on Warm
|
||||
Paper 1.31:1, Night Border on Deep Navy 1.65:1). Control edges use
|
||||
`--line-strong`, never `--line`.
|
||||
|
||||
## Gaps in the kit (recorded, not edited)
|
||||
|
||||
1. A control-edge color for each scheme. Slate and Mist pass 3:1 but are
|
||||
heavier than a border should be. Proposal: light `#7A8B95` (3.35:1 on Warm
|
||||
Paper, 3.53:1 on White, 3.10:1 on Seaglass); dark `#6F8CA3` (4.37:1 on Deep
|
||||
Navy, 3.35:1 on Ocean Depth, 3.47:1 on Night Surface).
|
||||
2. The kit has no ordered ramp of five dark grounds; Night Border doubles as
|
||||
the selected-row fill and the hairline.
|
||||
3. Fonts: the kit names Noto Sans; the site loads Shannon Sans subsets. Not a
|
||||
color, not changed here.
|
||||
|
||||
## Where codewhale-design differs
|
||||
|
||||
`vendor/codewhale-design/tokens.json` (GPUI 1.1.2) uses neutral warm grays
|
||||
(#28292b text, #faf8f5 paper, #202123 dark background) and the kit uses
|
||||
blue-tinted inks and navy grounds. The kit now leads on the site; the GPUI
|
||||
tokens differ on text, muted, lines, sidebar, dark grounds, live, attention and
|
||||
danger by a few units of lightness. Primary light (#245bc7) and danger light
|
||||
(#9e3f36) are identical. That repo was not edited.
|
||||
|
||||
## Imagery decisions
|
||||
|
||||
| Asset | Decision |
|
||||
| --- | --- |
|
||||
| `app/opengraph-image.tsx` | recolored to kit stops (generated in code) |
|
||||
| `public/whale/*.svg`, `app/icon.svg` | recolored belly to White; gradient stops were already kit |
|
||||
| `icon-192.png`, `icon-512.png`, `app/apple-icon.png` | belly pixels #F4F8FF recolored to White, no regeneration |
|
||||
| `codwhale-404.webp` | keep: the founder's restored 404 poster (see `ocean-contract.test.ts`), photographic parody art, name kept |
|
||||
| `codewhale-tui-*.png` | not touched: referenced only by README files, outside this slice |
|
||||
| `public/merch/` | keep: product renders |
|
||||
|
||||
## Two marks, two jobs (founder, 2026-10-11)
|
||||
|
||||
| Mark | Use for |
|
||||
| --- | --- |
|
||||
| Smooth aurora whale (`public/brand/mark-gradient.svg` family) | identity: app icon, favicon, header, OG image, print and merch |
|
||||
| Pixel-dither whale (`public/brand/pixel-whale.webp`, 192-cell dither, same aurora gradient and silhouette) | the terminal side: pet mode page, docs and README heroes, release art, TUI splash |
|
||||
|
||||
The pixel mark is a lossless raster (7.6 KB) shown with `image-rendering:
|
||||
pixelated`. The 404 poster stays as the founder restored it.
|
||||
`lib/gpui-role-tokens.test.ts` checks that every role resolves to its token
|
||||
and that every ink clears WCAG AA on every ground in every scheme.
|
||||
@@ -203,6 +203,7 @@
|
||||
"screenshotAlt": "Codewhale v{version}: a captured terminal view with the conversation, message composer and session controls.",
|
||||
"latestRelease": "Latest release {tag}",
|
||||
"releaseUnavailable": "Release status unavailable",
|
||||
"proofContributors": "{count} contributors",
|
||||
"currentSource": "Source",
|
||||
"sourceCandidate": "Unreleased",
|
||||
"publishedRelease": "released",
|
||||
|
||||
@@ -203,6 +203,7 @@
|
||||
"screenshotAlt": "Codewhale v{version} 的终端实录画面,包含对话、消息输入框与会话控制。",
|
||||
"latestRelease": "最新发布 {tag}",
|
||||
"releaseUnavailable": "发布状态暂不可用",
|
||||
"proofContributors": "{count} 位贡献者",
|
||||
"currentSource": "源码",
|
||||
"sourceCandidate": "未发布",
|
||||
"publishedRelease": "已发布",
|
||||
|
||||
@@ -5,13 +5,14 @@ export const RELEASE_HIGHLIGHTS = {
|
||||
comingLede: { en: "Merged on main and not in a published release yet. Build main to try it now.", zh: "已合并到 main,尚未包含在已发布版本中。现在可从 main 构建试用。" },
|
||||
changelogLink: { en: "Read the changelog", zh: "查看更新日志" },
|
||||
items: [
|
||||
{ title: "Steer mid-turn", body: { en: "Ctrl+Enter closes the in-flight model request and continues the same turn with your message. Tab and Shift+Tab change mode and permissions while the agent works; the change applies at its next step.", zh: "Ctrl+Enter 会立即结束正在进行的模型请求,并带着你的消息继续同一轮。智能体工作时也可以用 Tab 和 Shift+Tab 切换模式和权限,改动会在它的下一步生效。" } },
|
||||
{ title: "MCP", body: { en: "MCP servers now connect through the official MCP SDK (2.4) behind Codewhale's Rust broker. Newer servers are discovered with server/discover; older stdio and HTTP servers fall back to the legacy handshake.", zh: "MCP 服务器现在通过官方 MCP SDK(2.4)连接,由 Codewhale 的 Rust 代理把关。较新的服务器通过 server/discover 发现;较旧的 stdio 和 HTTP 服务器回退到旧版握手。" } },
|
||||
{ title: "MCP fixes", body: { en: "Older servers that reject the discovery probe still connect. A server that refuses sign-in shows its real 401 and the /mcp login hint, and one whose command cannot start says why.", zh: "拒绝发现探测的旧服务器仍可连接。拒绝登录的服务器会显示真实的 401 和 /mcp login 提示;命令无法启动的服务器会说明原因。" } },
|
||||
{ title: "DeepSeek Harness plugins", body: { en: "Reviewed DSH plugins run in the Native extension host. Tool output is checked against its declared schema, and unsupported options are refused instead of ignored.", zh: "经过审核的 DSH 插件在 Native 扩展宿主中运行。工具输出按声明的 schema 校验,不支持的选项会被拒绝而不是被忽略。" } },
|
||||
{ title: "Computer Use", body: { en: "No longer bundled in the binary. Install it from the codewhale marketplace, which points at its own release; every decision still needs your approval.", zh: "不再内置于二进制文件中。可从 codewhale 插件市场安装,市场指向它的独立发布版本;每个决定仍需你批准。" } },
|
||||
{ title: "Image-heavy sessions", body: { en: "Older images are left out of an outbound request with a receipt, and an HTTP 413 is retried once with a smaller body. Saved images stay intact.", zh: "较早的图片会从发出的请求中省略并留下记录;遇到 HTTP 413 时会用更小的请求体重试一次。已保存的图片保持不变。" } },
|
||||
{ title: "Instruction budgets", body: { en: "Global and project instructions get separate budgets, and every trim, skip or refusal shows in /context report.", zh: "全局指令和项目指令分别拥有各自的预算,每次截断、跳过或拒绝都会显示在 /context report 中。" } },
|
||||
{ title: "codewhale uninstall", body: { en: "On Unix, previews the release files the installer recorded; --yes removes only those. Your data and package-managed installs are kept.", zh: "在 Unix 上会预览安装程序记录的发布文件;--yes 只删除这些文件。你的数据和包管理器安装的版本会保留。" } },
|
||||
{ title: "codewhale uninstall", body: { en: "Previews the release files the installer recorded, on Unix and Windows; --yes removes only those. Package-managed copies print their own removal command. Your data stays unless you add --purge-data. Windows removal is compile-checked but not yet exercised on a Windows host.", zh: "在 Unix 和 Windows 上预览安装程序记录的发布文件;--yes 只删除这些文件。包管理器安装的版本会给出各自的卸载命令。除非加上 --purge-data,否则你的数据会保留。Windows 上的删除已通过编译检查,但尚未在 Windows 主机上实际运行。" } },
|
||||
{ title: "/rc", body: { en: "codewhale rc attaches to the hosted control plane for web Chat again: it accepts the exact released runtime identity instead of only the retired 0.9.13 one.", zh: "codewhale rc 可以再次连接托管控制平面以使用网页 Chat:它接受确切的已发布运行时身份,而不再只接受已退役的 0.9.13。" } },
|
||||
],
|
||||
};
|
||||
@@ -32,7 +32,7 @@ describe("design grammar contract", () => {
|
||||
expect(CSS).toMatch(/::selection \{ background: var\(--selection\); \}/);
|
||||
// The light ring on the dark stage would fall under 3:1, so the stage
|
||||
// re-declares it.
|
||||
expect(CSS).toMatch(/\.stage,\s*\.site-footer\s*\{[^}]*--ring: var\(--kit-bioluminescence\);/);
|
||||
expect(CSS).toMatch(/\.stage,\s*\.site-footer\s*\{[^}]*--ring: var\(--gpui-dark-accent\);/);
|
||||
});
|
||||
|
||||
it("paints every primary fill with the logo gradient and white text", () => {
|
||||
@@ -67,7 +67,8 @@ describe("design grammar contract", () => {
|
||||
it("times motion with the spring tokens and stills it under reduced motion", () => {
|
||||
expect(CSS).not.toMatch(/\d+ms ease[,;]/);
|
||||
expect(CSS).not.toMatch(/cubic-bezier\(0\.25, 0\.46/);
|
||||
expect(CSS).toMatch(/--ease-spring: cubic-bezier\(/);
|
||||
expect(CSS).toMatch(/--ease-spring: var\(--gpui-easing-panel\);/);
|
||||
expect(resolveWhale("var(--gpui-easing-panel)")).toMatch(/^cubic-bezier\(/);
|
||||
expect(CSS).toMatch(
|
||||
/@media \(prefers-reduced-motion: reduce\) \{\s*:root \{\s*--dur-state: 0ms;\s*--dur-spring: 0ms;/,
|
||||
);
|
||||
|
||||
@@ -97,7 +97,7 @@ describe("sitemap and hreflang preservation", () => {
|
||||
const existingEntries = sitemapEntries.filter(
|
||||
(entry) => new URL(entry.url).pathname.split("/")[2] !== "ratatui",
|
||||
);
|
||||
expect(existingEntries).toHaveLength(110);
|
||||
expect(existingEntries).toHaveLength(112);
|
||||
expect(sitemapEntries.filter(entry => entry.url.endsWith("/install")).map(entry => entry.url))
|
||||
.toEqual([`${SITE_URL}/en/install`]);
|
||||
expect(sitemapEntries.some(entry => entry.url.endsWith("/pricing"))).toBe(false);
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface RepoFacts {
|
||||
}
|
||||
|
||||
export const FACTS: RepoFacts = {
|
||||
"generatedAt": "2026-10-26T12:11:28.928Z",
|
||||
"generatedAt": "2026-10-27T22:46:48.361Z",
|
||||
"sourceRevision": null,
|
||||
"sourceCommittedAt": null,
|
||||
"version": "0.10.2",
|
||||
@@ -53,10 +53,12 @@ export const FACTS: RepoFacts = {
|
||||
"commands",
|
||||
"config",
|
||||
"core",
|
||||
"dependencies",
|
||||
"execpolicy",
|
||||
"hooks",
|
||||
"lane",
|
||||
"localization",
|
||||
"lsp",
|
||||
"mcp",
|
||||
"memory",
|
||||
"models",
|
||||
@@ -66,14 +68,18 @@ export const FACTS: RepoFacts = {
|
||||
"paths",
|
||||
"protocol",
|
||||
"release",
|
||||
"repl",
|
||||
"runtime",
|
||||
"sandbox",
|
||||
"sanitize",
|
||||
"secrets",
|
||||
"skill-install",
|
||||
"snapshot",
|
||||
"state",
|
||||
"telemetry",
|
||||
"tools",
|
||||
"tui",
|
||||
"util",
|
||||
"workflow",
|
||||
"workflow-js"
|
||||
],
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ export interface Feature {
|
||||
name: string;
|
||||
summary: string;
|
||||
status: FeatureStatus;
|
||||
surfaces: Partial<Record<"tui" | "api" | "web", SurfaceStatus>>;
|
||||
surfaces: Partial<Record<"tui" | "desktop" | "web" | "api", SurfaceStatus>>;
|
||||
since: string | null;
|
||||
flag: string | null;
|
||||
docs: string | null;
|
||||
|
||||
@@ -3,8 +3,8 @@ import { resolveWhale } from "./whale-tokens";
|
||||
import { siteCss } from "./site-css";
|
||||
|
||||
// The role layer (app/styles/tokens-roles.css): every role in every scheme,
|
||||
// inks from the Moda brand kit, the navy ocean grounds measured
|
||||
// against every ink at WCAG AA.
|
||||
// every value a tokens.json role, every ink measured against every
|
||||
// ground at WCAG AA.
|
||||
const CSS = siteCss();
|
||||
const ROLES = [
|
||||
"bg", "surface", "panel", "text", "muted", "line", "line-strong", "accent", "on-accent",
|
||||
@@ -62,21 +62,20 @@ describe("role tokens", () => {
|
||||
expect(CSS).not.toMatch(/@media \(prefers-color-scheme: dark\)/);
|
||||
});
|
||||
|
||||
it("inks with the Moda brand kit in both appearances", () => {
|
||||
const kit = (name: string) => hex(`var(--kit-${name})`);
|
||||
const inks: Record<string, Record<string, string>> = {
|
||||
light: { text: "ink", muted: "slate", accent: "signal-blue", ring: "signal-blue", live: "kelp-deep", attention: "amber", danger: "rust", "on-accent": "white" },
|
||||
dark: { text: "foam", muted: "mist", accent: "bioluminescence", ring: "bioluminescence", live: "kelp", attention: "lantern", danger: "coral", "on-accent": "abyss" },
|
||||
it("inks every role from the tokens.json set in both appearances", () => {
|
||||
const inks: Record<string, string> = {
|
||||
text: "text", muted: "text-muted", accent: "accent", ring: "accent", live: "live",
|
||||
attention: "attention", danger: "danger", "on-accent": "on-primary",
|
||||
bg: "bg", panel: "panel", surface: "sidebar", hover: "hover", selected: "list-active",
|
||||
"brand-fill": "primary", "on-brand": "on-primary",
|
||||
};
|
||||
for (const [mode, scheme] of [["light", light()], ["dark", pinnedDark()], ["dark", stage()]] as const) {
|
||||
for (const [role, name] of Object.entries(inks[mode])) {
|
||||
expect(scheme[role], `${mode}.${role}`).toBe(`var(--kit-${name})`);
|
||||
expect(hex(scheme[role]), `${mode}.${role}`).toBe(kit(name));
|
||||
for (const [mode, scheme, isStage] of [["light", light(), false], ["dark", pinnedDark(), false], ["dark", stage(), true]] as const) {
|
||||
for (const [role, token] of Object.entries(inks)) {
|
||||
if (isStage && role === "bg") continue;
|
||||
expect(scheme[role], `${mode}.${role}`).toBe(`var(--gpui-${mode}-${token})`);
|
||||
}
|
||||
}
|
||||
expect(hex(light().bg)).toBe(kit("warm-paper"));
|
||||
expect(hex(light().panel)).toBe(kit("white"));
|
||||
for (const role of GROUNDS) expect(pinnedDark()[role], role).toMatch(/^var\(--ocean-[\w-]+\)$/);
|
||||
expect(stage().bg).toBe("var(--gpui-dark-ocean-depth)");
|
||||
});
|
||||
|
||||
it("keeps every text ink at WCAG AA on every ground, in every scheme", () => {
|
||||
@@ -94,44 +93,17 @@ describe("role tokens", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("puts white text on the logo gradient only where it clears 4.5:1", () => {
|
||||
const fill = CSS.match(/--brand-fill:\s*linear-gradient\(([^;]+)\);/)?.[1] ?? "";
|
||||
const stops = [...fill.matchAll(/#[0-9a-f]{6}|var\(--brand-[\w-]+\)/gi)].map((m) => hex(m[0]));
|
||||
expect(stops.length).toBeGreaterThanOrEqual(2);
|
||||
for (const stop of stops) expect(contrast("#ffffff", stop), stop).toBeGreaterThanOrEqual(4.5);
|
||||
it("puts primary-foreground ink on every filled action at AA", () => {
|
||||
for (const [name, scheme] of [["light", light()], ["dark", pinnedDark()], ["stage", stage()]] as const) {
|
||||
expect(contrast(hex(scheme["on-brand"]), hex(scheme["brand-fill"])), `${name}: on-brand`).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
expect(hex("var(--brand-deep)")).toBe("#0b48bb");
|
||||
expect(hex("var(--brand-light)")).toBe("#1e8fd8");
|
||||
});
|
||||
|
||||
function stops(token: string): string[] {
|
||||
const ramp = CSS.match(new RegExp(`--${token}:\\s*linear-gradient\\(([^;]+)\\);`))?.[1] ?? "";
|
||||
return [...ramp.matchAll(/#[0-9a-f]{6}|var\(--[\w-]+\)/gi)].map((m) => hex(m[0]));
|
||||
}
|
||||
|
||||
it("keeps sea text readable on every stop of the sea ramps", () => {
|
||||
for (const token of ["sea", "sea-band"]) {
|
||||
const ramp = stops(token);
|
||||
expect(ramp.length, token).toBeGreaterThanOrEqual(3);
|
||||
for (const stop of ramp) {
|
||||
expect(contrast(hex(stage().muted), stop), `${token}: muted on ${stop}`).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(stage().text), stop), `${token}: text on ${stop}`).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps stage text readable over the deep tide stops", () => {
|
||||
// The tide runs paper -> aqua -> logo light -> logo deep -> mid -> navy.
|
||||
// Only its deep stops can carry stage text; the pale stops cannot.
|
||||
const ramp = stops("tide");
|
||||
const deep = ramp.indexOf(hex("var(--brand-deep)"));
|
||||
const mid = ramp.indexOf(hex("var(--sea-mid)"));
|
||||
expect(deep).toBeGreaterThan(0);
|
||||
expect(mid).toBeGreaterThan(deep);
|
||||
for (const stop of ramp.slice(deep)) {
|
||||
expect(contrast(hex(stage().text), stop), `text on ${stop}`).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
for (const stop of ramp.slice(mid)) {
|
||||
expect(contrast(hex(stage().muted), stop), `muted on ${stop}`).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
it("keeps stage text readable on the floor below the horizon", () => {
|
||||
const floor = hex("var(--ocean-floor)");
|
||||
expect(contrast(hex(stage().text), floor)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(stage().muted), floor)).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: لقطة طرفية تعرض المحادثة وحقل الرسالة وعناصر التحكم في الجلسة.",
|
||||
latestRelease: "أحدث إصدار {tag}",
|
||||
releaseUnavailable: "حالة الإصدار غير متاحة",
|
||||
proofContributors: "{count} مساهمًا",
|
||||
currentSource: "المصدر",
|
||||
sourceCandidate: "غير منشور",
|
||||
publishedRelease: "منشور",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: captura del terminal amb la conversa, el camp de missatge i els controls de sessió.",
|
||||
latestRelease: "Última versió {tag}",
|
||||
releaseUnavailable: "Estat de la versió no disponible",
|
||||
proofContributors: "{count} col·laboradors",
|
||||
currentSource: "Font",
|
||||
sourceCandidate: "Sense publicar",
|
||||
publishedRelease: "publicada",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: eine Terminalaufnahme mit Gespräch, Nachrichteneingabe und Sitzungssteuerung.",
|
||||
latestRelease: "Aktuellstes Release {tag}",
|
||||
releaseUnavailable: "Release-Status nicht verfügbar",
|
||||
proofContributors: "{count} Mitwirkende",
|
||||
currentSource: "Quelle",
|
||||
sourceCandidate: "Unveröffentlicht",
|
||||
publishedRelease: "veröffentlicht",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
export interface EnterpriseDict {
|
||||
metaTitle: string;
|
||||
metaDescription: string;
|
||||
kicker: string;
|
||||
status: string;
|
||||
title: string;
|
||||
lead: string;
|
||||
partnerCta: string;
|
||||
secondaryCta: string;
|
||||
loopTitle: string;
|
||||
loopScope: string;
|
||||
loop: { title: string; body: string }[];
|
||||
statusTitle: string;
|
||||
statusScope: string;
|
||||
todayTitle: string;
|
||||
today: { title: string; body: string }[];
|
||||
plannedTitle: string;
|
||||
planned: { title: string; body: string }[];
|
||||
controlTitle: string;
|
||||
dataTitle: string;
|
||||
dataBody: string;
|
||||
engineTitle: string;
|
||||
engineBody: string;
|
||||
partnerTitle: string;
|
||||
partnerBody: string;
|
||||
partnerEmail: string;
|
||||
}
|
||||
|
||||
export const enterprise: EnterpriseDict = {
|
||||
metaTitle: "Codewhale Enterprise · In development",
|
||||
metaDescription: "Codewhale Enterprise turns your team's work into evals, training data and models you own. In development; open to design partners.",
|
||||
kicker: "Codewhale Enterprise",
|
||||
status: "In development",
|
||||
title: "Own the intelligence your team runs on",
|
||||
lead: "Your team's work already runs through Codewhale. Enterprise turns that work into evals, training data and models you own. It is in development and open to design partners.",
|
||||
partnerCta: "Become a design partner",
|
||||
secondaryCta: "Try the open-source agent",
|
||||
|
||||
loopTitle: "What you get",
|
||||
loopScope: "Every model is tested on your own held-out tasks before it replaces anything.",
|
||||
loop: [
|
||||
{
|
||||
title: "Evals that define done",
|
||||
body: "An eval suite built from your team's real work, with a held-out set that decides whether a model is good enough.",
|
||||
},
|
||||
{
|
||||
title: "Training data from your work",
|
||||
body: "Data drawn from work your team already accepted, with sensitive details removed.",
|
||||
},
|
||||
{
|
||||
title: "A model you own",
|
||||
body: "A fine-tuned open-weight model you control, compared with your current model on quality, cost and speed.",
|
||||
},
|
||||
{
|
||||
title: "Environments that keep improving it",
|
||||
body: "RL environments built from your own tasks, so your model keeps improving on the work that matters to you.",
|
||||
},
|
||||
],
|
||||
|
||||
statusTitle: "Available today and planned",
|
||||
statusScope: "The open-source agent is available now. Enterprise is in development, and no customer model has been trained yet.",
|
||||
todayTitle: "Available today",
|
||||
today: [
|
||||
{
|
||||
title: "An agent for any model",
|
||||
body: "Hosted providers, any OpenAI-compatible endpoint, and models you serve yourself with vLLM, SGLang or Ollama.",
|
||||
},
|
||||
{
|
||||
title: "Saved sessions with receipts",
|
||||
body: "Every session keeps its tool calls, results and approvals, so your team's work is on record.",
|
||||
},
|
||||
],
|
||||
plannedTitle: "Planned for Enterprise",
|
||||
planned: [
|
||||
{
|
||||
title: "Managed eval and training environments",
|
||||
body: "Environments that run your evals and training without depending on a laptop.",
|
||||
},
|
||||
{
|
||||
title: "Qualification on every new base model",
|
||||
body: "When a better open model ships, it is tested on your holdout and promoted only if it passes.",
|
||||
},
|
||||
{
|
||||
title: "Routing between rented and owned models",
|
||||
body: "Each workflow goes to the model, rented or owned, with the best accepted result per cost.",
|
||||
},
|
||||
{
|
||||
title: "Private deployment",
|
||||
body: "Run the agent and Enterprise where your data must stay.",
|
||||
},
|
||||
],
|
||||
|
||||
controlTitle: "You keep control",
|
||||
dataTitle: "Your data stays where you choose",
|
||||
dataBody: "Your sessions, datasets and models stay in places you choose. Training runs only on compute you approve.",
|
||||
engineTitle: "One Rust engine with approvals",
|
||||
engineBody: "The same engine runs your workflows and Enterprise. Ask, Auto-Review and Full Access permissions and receipts apply to both.",
|
||||
|
||||
partnerTitle: "Design partners",
|
||||
partnerBody: "Bring one workflow your team repeats. You get a baseline of your current model on your own holdout, then a test of an owned model against it. Email us to start.",
|
||||
partnerEmail: "[email protected]",
|
||||
};
|
||||
@@ -17,6 +17,7 @@ export const home: HomeDict = {
|
||||
|
||||
latestRelease: "Latest release {tag}",
|
||||
releaseUnavailable: "Release status unavailable",
|
||||
proofContributors: "{count} contributors",
|
||||
currentSource: "Source",
|
||||
sourceCandidate: "Unreleased",
|
||||
publishedRelease: "released",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: captura del terminal con la conversación, el campo de mensaje y los controles de sesión.",
|
||||
latestRelease: "Último lanzamiento {tag}",
|
||||
releaseUnavailable: "Estado del lanzamiento no disponible",
|
||||
proofContributors: "{count} colaboradores",
|
||||
currentSource: "Fuente",
|
||||
sourceCandidate: "Sin publicar",
|
||||
publishedRelease: "publicado",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version} : capture du terminal avec la conversation, le champ de message et les commandes de session.",
|
||||
latestRelease: "Dernière version {tag}",
|
||||
releaseUnavailable: "État des versions indisponible",
|
||||
proofContributors: "{count} contributeurs",
|
||||
currentSource: "Source",
|
||||
sourceCandidate: "Non publiée",
|
||||
publishedRelease: "publiée",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: बातचीत, संदेश बॉक्स और सत्र नियंत्रण दिखाने वाला टर्मिनल कैप्चर।",
|
||||
latestRelease: "नवीनतम रिलीज़ {tag}",
|
||||
releaseUnavailable: "रिलीज़ स्थिति उपलब्ध नहीं",
|
||||
proofContributors: "{count} योगदानकर्ता",
|
||||
currentSource: "सोर्स",
|
||||
sourceCandidate: "अप्रकाशित",
|
||||
publishedRelease: "प्रकाशित",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: tangkapan terminal berisi percakapan, kolom pesan, dan kontrol sesi.",
|
||||
latestRelease: "Rilis terbaru {tag}",
|
||||
releaseUnavailable: "Status rilis tidak tersedia",
|
||||
proofContributors: "{count} kontributor",
|
||||
currentSource: "Sumber",
|
||||
sourceCandidate: "Belum dirilis",
|
||||
publishedRelease: "dirilis",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { docsClaudeCode as enDocsClaudeCode } from "./en/docs-claude-code";
|
||||
import { docsClaudeCode as zhDocsClaudeCode } from "./zh/docs-claude-code";
|
||||
import { enterprise as enEnterprise, type EnterpriseDict } from "./en/enterprise";
|
||||
import { enterprise as zhEnterprise } from "./zh/enterprise";
|
||||
import { docsSkills as enDocsSkills } from "./en/docs-skills";
|
||||
import { docsSkills as zhDocsSkills } from "./zh/docs-skills";
|
||||
/**
|
||||
@@ -640,6 +642,10 @@ export function getDocsSkills(locale: string): DocsPageDict {
|
||||
return locale === "zh" ? zhDocsSkills : enDocsSkills;
|
||||
}
|
||||
|
||||
export function getEnterprise(locale: string): EnterpriseDict {
|
||||
return locale === "zh" ? zhEnterprise : enEnterprise;
|
||||
}
|
||||
|
||||
export function getDocsClaudeCode(locale: string): DocsPageDict {
|
||||
return locale === "zh" ? zhDocsClaudeCode : enDocsClaudeCode;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: una cattura del terminale con conversazione, campo del messaggio e controlli della sessione.",
|
||||
latestRelease: "Ultima release {tag}",
|
||||
releaseUnavailable: "Stato delle release non disponibile",
|
||||
proofContributors: "{count} contributori",
|
||||
currentSource: "Sorgente",
|
||||
sourceCandidate: "Non rilasciata",
|
||||
publishedRelease: "rilasciata",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version} のターミナル収録画面。会話、メッセージ入力欄、セッション操作を表示。",
|
||||
latestRelease: "最新リリース {tag}",
|
||||
releaseUnavailable: "リリース情報を取得できません",
|
||||
proofContributors: "コントリビューター {count} 人",
|
||||
currentSource: "ソース",
|
||||
sourceCandidate: "未リリース",
|
||||
publishedRelease: "リリース済み",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: 대화, 메시지 입력창, 세션 제어를 보여 주는 실제 터미널 캡처.",
|
||||
latestRelease: "최신 릴리스 {tag}",
|
||||
releaseUnavailable: "릴리스 상태를 확인할 수 없음",
|
||||
proofContributors: "기여자 {count}명",
|
||||
currentSource: "소스",
|
||||
sourceCandidate: "미공개",
|
||||
publishedRelease: "공개됨",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: zrzut terminala z rozmową, polem wiadomości i kontrolkami sesji.",
|
||||
latestRelease: "Najnowsze wydanie {tag}",
|
||||
releaseUnavailable: "Status wydania niedostępny",
|
||||
proofContributors: "Współtwórcy: {count}",
|
||||
currentSource: "Źródło",
|
||||
sourceCandidate: "Niewydane",
|
||||
publishedRelease: "wydane",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: captura do terminal com conversa, campo de mensagem e controles da sessão.",
|
||||
latestRelease: "Último lançamento {tag}",
|
||||
releaseUnavailable: "Status do lançamento indisponível",
|
||||
proofContributors: "{count} colaboradores",
|
||||
currentSource: "Código-fonte",
|
||||
sourceCandidate: "Não publicado",
|
||||
publishedRelease: "publicado",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: снимок терминала с беседой, полем сообщения и управлением сессией.",
|
||||
latestRelease: "Последний релиз {tag}",
|
||||
releaseUnavailable: "Статус релиза недоступен",
|
||||
proofContributors: "Участников: {count}",
|
||||
currentSource: "Исходники",
|
||||
sourceCandidate: "Не выпущено",
|
||||
publishedRelease: "выпущено",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: sohbeti, mesaj alanını ve oturum kontrollerini gösteren terminal kaydı.",
|
||||
latestRelease: "En yeni sürüm {tag}",
|
||||
releaseUnavailable: "Sürüm durumu kullanılamıyor",
|
||||
proofContributors: "{count} katkıda bulunan",
|
||||
currentSource: "Kaynak",
|
||||
sourceCandidate: "Yayımlanmadı",
|
||||
publishedRelease: "yayımlandı",
|
||||
|
||||
@@ -162,6 +162,7 @@ export interface HomeDict {
|
||||
/** "Latest release {tag}" */
|
||||
latestRelease: string;
|
||||
releaseUnavailable: string;
|
||||
proofContributors: string;
|
||||
/** "Source" / "Unreleased" — prepended to `v{version}`. */
|
||||
currentSource: string;
|
||||
sourceCandidate: string;
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: знімок термінала з розмовою, полем повідомлення й керуванням сесією.",
|
||||
latestRelease: "Останній реліз {tag}",
|
||||
releaseUnavailable: "Статус релізу недоступний",
|
||||
proofContributors: "Учасників: {count}",
|
||||
currentSource: "Джерело",
|
||||
sourceCandidate: "Не випущено",
|
||||
publishedRelease: "випущено",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version}: ảnh chụp terminal với cuộc trò chuyện, ô nhập tin nhắn và điều khiển phiên.",
|
||||
latestRelease: "Bản phát hành mới nhất {tag}",
|
||||
releaseUnavailable: "Không có trạng thái phát hành",
|
||||
proofContributors: "{count} người đóng góp",
|
||||
currentSource: "Mã nguồn",
|
||||
sourceCandidate: "Chưa phát hành",
|
||||
publishedRelease: "đã phát hành",
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { EnterpriseDict } from "../en/enterprise";
|
||||
|
||||
export const enterprise: EnterpriseDict = {
|
||||
metaTitle: "Codewhale Enterprise · 开发中",
|
||||
metaDescription: "Codewhale Enterprise 把团队的工作变成评测、训练数据和你自己拥有的模型。开发中,欢迎设计合作伙伴。",
|
||||
kicker: "Codewhale Enterprise",
|
||||
status: "开发中",
|
||||
title: "拥有团队赖以工作的智能",
|
||||
lead: "团队的工作已经在 Codewhale 中进行。Enterprise 把这些工作变成评测、训练数据和你自己拥有的模型。目前处于开发阶段,欢迎设计合作伙伴加入。",
|
||||
partnerCta: "成为设计合作伙伴",
|
||||
secondaryCta: "试用开源智能体",
|
||||
|
||||
loopTitle: "你将得到什么",
|
||||
loopScope: "每个模型在替换任何东西之前,都先在你自己的保留任务上测试。",
|
||||
loop: [
|
||||
{
|
||||
title: "定义“完成”的评测",
|
||||
body: "基于团队真实工作构建的评测套件,由保留集判断模型是否够好。",
|
||||
},
|
||||
{
|
||||
title: "来自你工作的训练数据",
|
||||
body: "数据取自团队已经认可的工作,并去除敏感信息。",
|
||||
},
|
||||
{
|
||||
title: "你自己拥有的模型",
|
||||
body: "由你掌控的微调开放权重模型,并在质量、成本和速度上与当前模型对比。",
|
||||
},
|
||||
{
|
||||
title: "持续改进模型的环境",
|
||||
body: "基于你自己的任务构建的强化学习环境,让模型在你最在意的工作上持续变好。",
|
||||
},
|
||||
],
|
||||
|
||||
statusTitle: "现已可用与计划中",
|
||||
statusScope: "开源智能体现已可用。Enterprise 正在开发中,目前还没有训练过客户模型。",
|
||||
todayTitle: "现已可用",
|
||||
today: [
|
||||
{
|
||||
title: "支持任何模型的智能体",
|
||||
body: "托管服务商、任何 OpenAI 兼容端点,以及你用 vLLM、SGLang 或 Ollama 自行部署的模型。",
|
||||
},
|
||||
{
|
||||
title: "带回执的已保存会话",
|
||||
body: "每个会话都保留工具调用、结果和审批记录,团队的工作有据可查。",
|
||||
},
|
||||
],
|
||||
plannedTitle: "Enterprise 计划",
|
||||
planned: [
|
||||
{
|
||||
title: "托管的评测与训练环境",
|
||||
body: "运行评测和训练的环境,不再依赖笔记本电脑。",
|
||||
},
|
||||
{
|
||||
title: "每个新基座模型都重新验证",
|
||||
body: "更好的开放模型发布时,先在你的保留集上测试,通过后才会上线。",
|
||||
},
|
||||
{
|
||||
title: "在租用模型与自有模型之间路由",
|
||||
body: "每个工作流都交给每单位成本下被接受结果最好的模型,无论租用还是自有。",
|
||||
},
|
||||
{
|
||||
title: "私有部署",
|
||||
body: "在数据必须停留的地方运行智能体和 Enterprise。",
|
||||
},
|
||||
],
|
||||
|
||||
controlTitle: "由你掌控",
|
||||
dataTitle: "数据留在你选择的地方",
|
||||
dataBody: "你的会话、数据集和模型都留在你选择的地方。训练只在你批准的算力上进行。",
|
||||
engineTitle: "同一个带审批的 Rust 引擎",
|
||||
engineBody: "运行工作流和 Enterprise 的是同一个引擎。Ask、Auto-Review 和 Full Access 权限以及回执对两者同样适用。",
|
||||
|
||||
partnerTitle: "设计合作伙伴",
|
||||
partnerBody: "带来一个团队反复做的工作流。你会得到当前模型在你自己保留集上的基线,再测试一个自有模型与之对比。发邮件即可开始。",
|
||||
partnerEmail: "[email protected]",
|
||||
};
|
||||
@@ -15,6 +15,7 @@ export const home: HomeDict = {
|
||||
"Codewhale v{version} 的终端实录画面,包含对话、消息输入框与会话控制。",
|
||||
latestRelease: "最新发布 {tag}",
|
||||
releaseUnavailable: "发布状态暂不可用",
|
||||
proofContributors: "{count} 位贡献者",
|
||||
currentSource: "源码",
|
||||
sourceCandidate: "未发布",
|
||||
publishedRelease: "已发布",
|
||||
|
||||
@@ -58,7 +58,7 @@ describe("the horizon", () => {
|
||||
});
|
||||
|
||||
it("paints the stage with the dark set in both appearances", () => {
|
||||
expect(CSS).toMatch(/\.stage,\s*\.site-footer\s*\{[^}]*--ring: var\(--kit-bioluminescence\);[^}]*color-scheme: dark;/);
|
||||
expect(CSS).toMatch(/\.stage,\s*\.site-footer\s*\{[^}]*--ring: var\(--gpui-dark-accent\);[^}]*color-scheme: dark;/);
|
||||
});
|
||||
|
||||
it("keeps the horizon visible in forced colours", () => {
|
||||
|
||||
@@ -41,7 +41,13 @@ export const RELEASE_CONTRIBUTORS: string[] = [
|
||||
* one. scripts/check-contributor-credit.py requires them here now; the release
|
||||
* cut moves them into RELEASE_CONTRIBUTORS with that release's changelog block.
|
||||
*/
|
||||
export const UNRELEASED_CONTRIBUTORS: string[] = [];
|
||||
export const UNRELEASED_CONTRIBUTORS: string[] = [
|
||||
"@7jrxt42BxFZo4iAnN4CX",
|
||||
"@Lstarsky0",
|
||||
"@gaord",
|
||||
"@SparkofSpike",
|
||||
"@Aboysky",
|
||||
];
|
||||
|
||||
/**
|
||||
* Contributors who helped with reports, reproductions, and verification.
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("typography contract", () => {
|
||||
expect(rootVar("font-mono")).toMatch(/^ui-monospace,/);
|
||||
expect(rootVar("font-cjk")).not.toMatch(/Serif|(?<!sans-)serif/);
|
||||
expect(resolveWhale(rootVar("text-mono"))).toBe("0.8125rem");
|
||||
expect(rootVar("text-prose")).toBe("1.0625rem");
|
||||
expect(resolveWhale(rootVar("text-prose"))).toBe("1rem");
|
||||
});
|
||||
|
||||
it("preloads only the Latin subset of Shannon Sans", () => {
|
||||
|
||||
@@ -27,6 +27,7 @@ const FILES = ["chrome.ts", "home.ts"];
|
||||
// lookup time — so absence is a valid state, never a failure.
|
||||
const OPTIONAL_FILES = [
|
||||
"features.ts",
|
||||
"enterprise.ts",
|
||||
"docs-skills.ts",
|
||||
"docs-claude-code.ts",
|
||||
"docs-guide.ts",
|
||||
|
||||
@@ -160,7 +160,7 @@ export function deriveFeatures() {
|
||||
}
|
||||
ids.add(row.id);
|
||||
for (const [surface, state] of Object.entries(row.surfaces)) {
|
||||
if (!["tui", "api", "web"].includes(surface) || !["stable", "partial", "preview", "none"].includes(state)) {
|
||||
if (!["tui", "desktop", "web", "api"].includes(surface) || !["stable", "partial", "preview", "none"].includes(state)) {
|
||||
throw new Error(`invalid feature surface: ${row.id}/${surface}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user