Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/agent-core-dev/orient.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ There is no domain-layer numbering — a domain may import any other domain, gui

## Comment convention

`packages/agent-core-v2/AGENTS.md` bans comments: no file headers, no section banners, no statement-level narration — the code is the source of truth. The only exception is JSDoc attached to exported symbols, which flows into the generated `.d.ts` and the consumers' IDE hover. Tooling directives (`eslint-disable`, `@ts-expect-error`, …) are banned too: fix the underlying lint/type problem instead, and put negative type-safety cases in compiler-asserted fixtures. DI scope is carried by registration: `LifecycleScope.App`, `LifecycleScope.Session`, or `LifecycleScope.Agent`. A `workspace*` filename marks workspace-domain ownership, not a DI scope (see service-authoring.md).
`packages/agent-core-v2/AGENTS.md` bans comments entirely: no file headers, no section banners, no statement-level narration, no JSDoc (not even on exported symbols) — the code is the source of truth. The only exception is a load-bearing lint-suppression directive (`oxlint-disable` / `eslint-disable`) for a deliberate pattern; other tooling directives (`@ts-expect-error`, …) are banned: fix the underlying lint/type problem instead, and put negative type-safety cases in compiler-asserted fixtures. DI scope is carried by registration: `LifecycleScope.App`, `LifecycleScope.Session`, or `LifecycleScope.Agent`. A `workspace*` filename marks workspace-domain ownership, not a DI scope (see service-authoring.md).

## Red lines (this stage)

- Import via the `#/...` alias (mapped to `src/`); never reach into another domain's internals by relative path.
- Short-lived may inject long-lived; never the reverse.
- No comments — not file headers, not beside statements; exported-symbol JSDoc is the only exception.
- No comments — not file headers, not beside statements, not JSDoc; a load-bearing lint-suppression directive is the only exception.
5 changes: 2 additions & 3 deletions .agents/skills/agent-core-dev/service-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ What belongs here:
- **Helper classes / functions** used only by this impl (e.g. a built-in writer, an `extractError` helper) — co-located in the same file.
- **Top-level `registerScopedService(...)`** — one per Service the file owns; importing the impl file runs the registration.

Base class: extend `Service` (from `#/_base/di/service`) when the unit needs capability calls on `this` — `provide` / `effect` / `on` / `get` / `ref` (e.g. contributing a record to a `collection` token). `Service` extends `Disposable`, so `_register` keeps working; constructor-time `provide` / `on` / `effect` calls are buffered and flushed by the kernel after construction, while `get` / `ref` throw inside the constructor (dependencies stay constructor parameters). Otherwise extend `Disposable` — both are full DI units; a service whose own members collide with the `Service` vocabulary (`name` / `state` / `config` / `get`) must stay on `Disposable` (leave a NOTE comment saying so).
Base class: extend `Service` (from `#/_base/di/service`) when the unit needs capability calls on `this` — `provide` / `effect` / `on` / `get` / `ref` (e.g. contributing a record to a `collection` token). `Service` extends `Disposable`, so `_register` keeps working; constructor-time `provide` / `on` / `effect` calls are buffered and flushed by the kernel after construction, while `get` / `ref` throw inside the constructor (dependencies stay constructor parameters). Otherwise extend `Disposable` — both are full DI units; a service whose own members collide with the `Service` vocabulary (`name` / `state` / `config` / `get`) must stay on `Disposable`.

## Constructor conventions

Expand Down Expand Up @@ -296,9 +296,8 @@ Importing the package therefore fires every `register*` side effect, exactly as

## Comments

- **No comments** (orient.md): no file headers, no statement-level narration; the only exception is JSDoc attached to exported symbols.
- **No comments** (orient.md): no file headers, no statement-level narration, no JSDoc; the only exception is a load-bearing lint-suppression directive.
- **Methods and fields carry no comments by default.** Well-named identifiers and types say *what*; the code is the source of truth for *how*.
- Write an inline comment only when the *why* is non-obvious (a hidden constraint, a subtle invariant, a workaround). One short line.
- For unimplemented stubs, throw `NotImplementedError('feature')` rather than `throw new Error('TODO: …')` (errors.md).

## Complete minimal example
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/agent-core-dev/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Resolving by interface is what makes `registerScopedService(ISut, Sut, …)` par

Pure functions, value objects, and services with **no** `@IService` dependencies may be constructed directly.

The only other exception is a test that genuinely needs **two independent instances** of the same service with different dependencies (e.g. constructing two `TurnService`s with different `ILoopRunner`s). A singleton-per-container resolution cannot produce both, so `ix.createInstance(Impl)` is acceptable there — annotate it with a comment explaining why.
The only other exception is a test that genuinely needs **two independent instances** of the same service with different dependencies (e.g. constructing two `TurnService`s with different `ILoopRunner`s). A singleton-per-container resolution cannot produce both, so `ix.createInstance(Impl)` is acceptable there — state the reason in the test name and local identifiers.

## Two harnesses

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/agent-core-dev/verify.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Walk the stages you touched and confirm:
- **Design** — scope follows state identity; no `Map<sessionId, …>` at `App`; dependency arrows do not make a foundational layer know an upstream one; no cycle was routed around.
- **Implement** — no `new` on `@IService`-carrying classes; `@IX` on constructor params only (service params after static params); interface + impl carry `_serviceBrand`; decorator names unique; coded errors only; flags for unreleased behavior.
- **Test** — SUT resolved by interface; stubs under `test/`; scope tests re-register after `_clearScopedRegistryForTests()`; teardown through one `DisposableStore`.
- **Files** — no comments (exported-symbol JSDoc excepted); registration runs from the impl file's top level; the new domain is exported from `src/index.ts`.
- **Files** — no comments (no JSDoc either); registration runs from the impl file's top level; the new domain is exported from `src/index.ts`.

Then re-read the [global red lines](SKILL.md#global-red-lines) once — they catch most cross-stage mistakes in a single scan.

Expand Down
5 changes: 5 additions & 0 deletions .changeset/abort-signal-listener-ceiling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Silence the MaxListenersExceededWarning that could appear during long agent turns with many parallel tool calls.
5 changes: 5 additions & 0 deletions .changeset/broadcast-user-prompts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Fix messages sent from one web client not appearing on other clients connected to the same session.
5 changes: 5 additions & 0 deletions .changeset/drop-allow-remote-terminals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": major
---

Remove the `--allow-remote-terminals` flag from `pythinker web`; PTY terminal routes now stay available on loopback binds only.
5 changes: 5 additions & 0 deletions .changeset/effort-persist-default-ceiling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Persist a picked thinking effort as the default only up to the model's own default effort; a more expensive pick stays session-only.
5 changes: 5 additions & 0 deletions .changeset/infinite-retry-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": minor
---

Add `PYTHINKER_CODE_INFINITE_RETRY=1` to retry every failed model request indefinitely with backoff instead of failing the turn, for long unattended runs.
5 changes: 5 additions & 0 deletions .changeset/plugins-marketplace-async-versions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Show the /plugins marketplace catalog as soon as it loads, with latest-version lookups running in the background.
5 changes: 5 additions & 0 deletions .changeset/tasks-run-in-background.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Fix foreground subagents being reported as background tasks on the task list.
5 changes: 5 additions & 0 deletions .changeset/vscode-effort-persist-default-ceiling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"pythinker": patch
---

Persist a picked thinking effort as the default only up to the model's own default effort; a more expensive pick stays session-only.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ The web bundle: `apps/pythinker-code/dist-web` is the committed, prebuilt bundle
## Coding Rules

- English-only codebase. Use ASCII/Latin fixtures (e.g. `café`) for unicode tests.
- `packages/agent-core-v2`, `packages/agent-gateway`, and `packages/transcript` are comment-free zones: no line/block comments; exceptions are JSDoc attached to exported symbols and load-bearing lint-suppression directives (`oxlint-disable` / `eslint-disable`), while other tooling directives (`@ts-expect-error`, …) stay banned. Enforced by `scripts/check-no-comments.mjs`, which runs as part of `pnpm lint`.
- `packages/agent-core-v2`, `packages/agent-gateway`, and `packages/transcript` are comment-free zones: no line/block comments; no JSDoc either, not even on exported symbols; the only exception is a load-bearing lint-suppression directive (`oxlint-disable` / `eslint-disable`), while other tooling directives (`@ts-expect-error`, …) stay banned. Enforced by `scripts/check-no-comments.mjs`, which runs as part of `pnpm lint`.
- `packages/acp-adapter`: pin `@agentclientprotocol/sdk` `^0.23.0` (0.24+ broke session-model API).
- `tsgo` (`@typescript/native-preview`) available via `npx tsgo -p <tsconfig> --noEmit`; committed scripts use `tsc` — run both for type fixes.
- Pass `undefined` directly for optional props — no conditional spread.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo

## General Coding Rules

- `packages/agent-core-v2`, `packages/agent-gateway`, and `packages/transcript` are comment-free zones: no line/block comments; the exceptions are JSDoc attached to exported symbols and load-bearing lint-suppression directives (`oxlint-disable` / `eslint-disable`), while other tooling directives (`@ts-expect-error`, …) stay banned. Enforced by `scripts/check-no-comments.mjs`, which runs as part of `pnpm lint`.
- `packages/agent-core-v2`, `packages/agent-gateway`, and `packages/transcript` are comment-free zones: no line/block comments; no JSDoc either, not even on exported symbols; the only exception is a load-bearing lint-suppression directive (`oxlint-disable` / `eslint-disable`), while other tooling directives (`@ts-expect-error`, …) stay banned. Enforced by `scripts/check-no-comments.mjs`, which runs as part of `pnpm lint`.
- For optional object properties, pass `undefined` directly instead of using conditional spread.
- YES: `{ user }`
- NO: `{ ...(user ? { user } : undefined) }`
Expand Down
2 changes: 1 addition & 1 deletion apps/pythinker-code/dist-web/.web-bundle-manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"sourceHash": "3a46f9dff05369ad09252708dcc377f6495aadcb00e6bcea32130261bd380df2",
"sourceHash": "f1f4f846df4abed27745e6cf05a8cf9a6411b3e5b8e0a4cc66344de574dc5edd",
"sourceFileCount": 399
}
6 changes: 0 additions & 6 deletions apps/pythinker-code/src/cli/sub/web/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,6 @@ export function buildWebCommand(cmd: Command): Command {
'On a non-loopback bind, keep POST /api/v1/shutdown enabled (default: route is disabled → 404).',
false,
)
.option(
'--allow-remote-terminals',
'On a non-loopback bind, keep the PTY /api/v1/terminals/* routes enabled (default: disabled → 404). Remote shell is high risk.',
false,
)
.option(
'--dangerous-bypass-auth',
'Disable bearer-token auth on every REST and WebSocket route, and advertise it via /api/v1/meta so the web UI connects without a token. Only use on a trusted network or behind your own authenticating proxy.',
Expand Down Expand Up @@ -300,7 +295,6 @@ async function runServerInProcess(
debugEndpoints: options.debugEndpoints,
insecureNoTls: options.insecureNoTls,
allowRemoteShutdown: options.allowRemoteShutdown,
allowRemoteTerminals: options.allowRemoteTerminals,
allowedHosts: options.allowedHosts,
disableAuth: options.dangerousBypassAuth,
webTitle: options.webTitle,
Expand Down
5 changes: 0 additions & 5 deletions apps/pythinker-code/src/cli/sub/web/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ export interface ParsedServerOptions {
insecureNoTls: boolean;
/** Allow `POST /api/v1/shutdown` on a non-loopback bind. */
allowRemoteShutdown: boolean;
/** Allow PTY `/api/v1/terminals/*` routes on a non-loopback bind. */
allowRemoteTerminals: boolean;
/** Disable bearer-token auth on every route (`--dangerous-bypass-auth`). */
dangerousBypassAuth: boolean;
/** Extra `Host` header values to allow through the DNS-rebinding check. */
Expand All @@ -59,8 +57,6 @@ export interface ServerCliOptions {
insecureNoTls?: boolean;
/** Allow remote shutdown on a non-loopback bind (`--allow-remote-shutdown`). */
allowRemoteShutdown?: boolean;
/** Allow remote terminals on a non-loopback bind (`--allow-remote-terminals`). */
allowRemoteTerminals?: boolean;
/** Disable bearer-token auth on every route (`--dangerous-bypass-auth`). */
dangerousBypassAuth?: boolean;
/** Extra `Host` header values to allow (`--allowed-host`). */
Expand All @@ -77,7 +73,6 @@ export function parseServerOptions(opts: ServerCliOptions): ParsedServerOptions
debugEndpoints: opts.debugEndpoints === true,
insecureNoTls: opts.insecureNoTls !== false,
allowRemoteShutdown: opts.allowRemoteShutdown === true,
allowRemoteTerminals: opts.allowRemoteTerminals === true,
dangerousBypassAuth: opts.dangerousBypassAuth === true,
allowedHosts: parseAllowedHostArgs(opts.allowedHost),
webTitle: opts.webTitle,
Expand Down
4 changes: 4 additions & 0 deletions apps/pythinker-code/src/constant/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,7 @@ export const OAUTH_LOGIN_REQUIRED_CODE = ErrorCodes.AUTH_LOGIN_REQUIRED;
export {
PYTHINKER_CODE_PLUGIN_MARKETPLACE_URL_ENV,
} from '@pymodel/agent-core-v2/app/plugin/marketplace';
// Bound on each background "latest release" lookup when the TUI fills in
// marketplace versions. Without it a stalled connection to github.com hangs
// the version phase for undici's default header timeout (300s).
export const MARKETPLACE_VERSION_LOOKUP_TIMEOUT_MS = 5000;
2 changes: 1 addition & 1 deletion apps/pythinker-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ async function persistModelSelection(
const model = host.state.appState.availableModels[alias];
const full = thinkingEffortToConfig(
effort,
model === undefined ? undefined : effectiveModelForHost(host, model).supportEfforts,
model === undefined ? undefined : effectiveModelForHost(host, model),
);
// Re-confirming the effort shown when the picker opened is not an explicit
// choice — persist the model but leave the stored effort preference alone.
Expand Down
49 changes: 43 additions & 6 deletions apps/pythinker-code/src/tui/commands/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ import {
isOfficialPluginSource,
} from '../utils/plugin-source-label';
import { PYTHINKER_CODE_PLUGIN_MARKETPLACE_URL_ENV } from '#/constant/app';
import { loadPluginMarketplace, type PluginMarketplaceEntry } from '#/utils/plugin-marketplace';
import {
loadPluginMarketplace,
withBuiltInEntries,
withMarketplaceLatestVersions,
type PluginMarketplace,
type PluginMarketplaceEntry,
} from '#/utils/plugin-marketplace';
import type { SlashCommandHost } from './dispatch';

interface ShowPluginsPickerOptions {
Expand Down Expand Up @@ -343,18 +349,49 @@ async function loadMarketplaceCatalog(
source: string | undefined,
capabilities: readonly CapabilityStatus[],
): Promise<void> {
const builtInEntries =
host.engineV2 && isDefaultMarketplaceCatalog(source)
? capabilities.map(capabilityMarketplaceEntry)
: undefined;
let marketplace: PluginMarketplace;
let catalog: PluginMarketplace;
try {
const marketplace = await loadPluginMarketplace({
// Phase 1: render the catalog as soon as it arrives. Version lookups
// (GitHub releases/latest round trips) must not gate the first paint.
// Keep the raw parsed catalog for phase 2: injecting built-ins first
// would mask the matching catalog entries' GitHub sources behind
// `capability:<id>` rows, making their versions unresolvable.
catalog = await loadPluginMarketplace({
workDir: host.state.appState.workDir,
source,
builtInEntries:
host.engineV2 && isDefaultMarketplaceCatalog(source)
? capabilities.map(capabilityMarketplaceEntry)
: undefined,
skipLatestVersions: true,
});
marketplace =
builtInEntries !== undefined ? withBuiltInEntries(catalog, builtInEntries) : catalog;
panel.setMarketplace(marketplace.plugins, marketplace.source);
host.state.ui.requestRender();
} catch (error) {
// Any phase-1 failure (unreachable OR malformed catalog) surfaces as an
// error: the panel keeps built-in capability rows installable in the
// Official tab while the error is shown, and a broken catalog must not
// be masked as a successfully loaded, built-ins-only marketplace.
panel.setMarketplaceError(formatErrorMessage(error));
host.state.ui.requestRender();
return;
}
try {
// Phase 2: resolve latest versions in the background (against the raw
// catalog), re-apply the built-in injection so resolved versions flow
// onto capability rows, then refresh so update badges appear. Failures
// degrade to badge-less rows and never clobber the rendered list.
const enrichedCatalog = await withMarketplaceLatestVersions(catalog);
const enriched =
builtInEntries !== undefined
? withBuiltInEntries(enrichedCatalog, builtInEntries)
: enrichedCatalog;
panel.setMarketplace(enriched.plugins, enriched.source);
} catch (error) {
log.warn('marketplace version lookup failed', { error });
}
host.state.ui.requestRender();
}
Expand Down
19 changes: 13 additions & 6 deletions apps/pythinker-code/src/tui/commands/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,24 +295,31 @@ async function handleCatalogProviderAdd(host: SlashCommandHost): Promise<void> {
host.mountEditorReplacement(selector);
}

async function setDefaultModel(
export async function setDefaultModel(
host: SlashCommandHost,
alias: string,
effort: ThinkingEffort,
): Promise<void> {
// Resolve efforts the same way the /model path does (effectiveModelForHost
// applies overrides and the protocol-profile inference): catalog entries for
// e.g. Anthropic models declare no support_efforts on the alias, and without
// the inference a top-tier pick would slip through as a persisted effort.
// the inference an above-default pick would slip through as a persisted effort.
const model = host.state.appState.availableModels[alias];
const thinking = thinkingEffortToConfig(
effort,
model === undefined ? undefined : effectiveModelForHost(host, model),
);
await host.harness.setConfig({
defaultModel: alias,
thinking: thinkingEffortToConfig(
effort,
model === undefined ? undefined : effectiveModelForHost(host, model).supportEfforts,
),
thinking,
});
await host.authFlow.refreshConfigAfterLogin();
// refreshConfigAfterLogin reactivates from the persisted config, so a pick
// the gate keeps session-only never reaches the runtime — apply it after
// the refresh, or the persisted value would clobber it.
if (thinking.effort === undefined && effort !== 'off' && effort !== 'on') {
await host.authFlow.activateModelAfterLogin(alias, effort);
}
host.track('model_switch', { model: alias });
host.showStatus(`Default model set to ${alias} with thinking ${effort}.`);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/pythinker-code/src/tui/controllers/editor-keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ export class EditorKeyboardController {
const harness = this.host.harness;
if (harness === undefined || alias !== this.host.state.appState.model) return;
try {
await harness.setConfig({ thinking: thinkingEffortToConfig(effort, model.supportEfforts) });
await harness.setConfig({ thinking: thinkingEffortToConfig(effort, model) });
} catch (error) {
this.host.showError(
`Thinking effort set to ${effort}, but failed to save default: ${formatErrorMessage(error)}`,
Expand Down
Loading
Loading