mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
Merge main into native chat SQLite host
This commit is contained in:
@@ -45,6 +45,9 @@ export type ControlAttachment = {
|
||||
clientKeepalive?: boolean
|
||||
// A keepalive control found silent: one relay ping sent at this time (see probeSilentControls).
|
||||
probeAt?: number
|
||||
// Upgrade and activation times, for metrics (absent on attachments from before these fields).
|
||||
openedAt?: number
|
||||
activeAt?: number
|
||||
}
|
||||
export type Attachment = ControlAttachment
|
||||
export type Conn = {
|
||||
|
||||
@@ -28,8 +28,16 @@ export abstract class HostRelayControl extends HostRelayHandshake {
|
||||
// Where the host is going away anyway (export, erase, drain, a new control of the same host): a
|
||||
// silent control is closed now. Its orphan time is the latest the ping alarm would have found it,
|
||||
// so rebind and the orphan grace are never shorter than before.
|
||||
// A control closed for silence: value = seconds silent, label = where it was found, reason =
|
||||
// keepalive | legacy; double2 = seconds since it became active (-1 if unknown).
|
||||
protected deadControlMetric(a: ControlAttachment, now: number, seen: number, found: 'alarm' | 'on_demand' | 'expired'): void {
|
||||
const ageS = a.activeAt === undefined ? -1 : Math.round((now - a.activeAt) / 1000)
|
||||
metric(this.env, 'silence_close', Math.round((now - seen) / 1000), found, [ageS], this.keepalive(a) ? 'keepalive' : 'legacy', a.place)
|
||||
}
|
||||
|
||||
protected async closeSilentControls(now = Date.now()): Promise<void> {
|
||||
for (const { ws, seen } of this.silentControls(now)) {
|
||||
for (const { ws, a, seen } of this.silentControls(now)) {
|
||||
this.deadControlMetric(a, now, seen, 'on_demand')
|
||||
this.close(ws, RELAY_CLOSE_CODE.PEER_DROPPED, 'control silence timeout')
|
||||
await this.onSocketGone(ws, RELAY_CLOSE_CODE.PEER_DROPPED, '', Math.min(now, seen + this.config.silenceTimeoutMs + this.config.pingIntervalMs))
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ export abstract class HostRelayHandshake extends HostRelayBase {
|
||||
})
|
||||
const leaseExpiresAt = now + LEASE_BASE_MS + Math.round((Math.random() * 2 - 1) * LEASE_JITTER_MS)
|
||||
a.phase = 'active'
|
||||
a.activeAt = now
|
||||
a.generation = generation
|
||||
a.lastPongAt = now
|
||||
a.lastPingAt = now
|
||||
@@ -191,7 +192,7 @@ export abstract class HostRelayHandshake extends HostRelayBase {
|
||||
})
|
||||
// After the ack, which strict shipped parsers must see unchanged; only hosts that asked get it.
|
||||
if (a.clientKeepalive) { this.send(ws, { type: 'keepalive-offer', v: 1, intervalMs: this.config.pingIntervalMs }) }
|
||||
metric(this.env, 'control_active', 1, rebind ? 'rebind' : 'new', [], '', a.place)
|
||||
metric(this.env, 'control_active', 1, rebind ? 'rebind' : 'new', [a.openedAt === undefined ? -1 : now - a.openedAt], a.clientKeepalive ? 'keepalive' : 'legacy', a.place)
|
||||
await this.reschedule()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,8 @@ export class HostRelay extends HostRelayPhones {
|
||||
claims,
|
||||
origin,
|
||||
caps: [...parseRelayHostCapabilities(request.headers.get('x-orca-host-capabilities') ?? '')],
|
||||
deadline: Date.now() + RELAY_PROTOCOL_LIMITS.firstFrameDeadlineMs
|
||||
deadline: Date.now() + RELAY_PROTOCOL_LIMITS.firstFrameDeadlineMs,
|
||||
openedAt: Date.now()
|
||||
})
|
||||
await this.reschedule()
|
||||
return new Response(null, { status: 101, webSocket: client })
|
||||
@@ -180,14 +181,25 @@ export class HostRelay extends HostRelayPhones {
|
||||
continue
|
||||
}
|
||||
if (now - seen > this.config.silenceTimeoutMs && (!this.keepalive(a) || now - a.probeAt! >= KEEPALIVE_PROBE_MS - 50)) {
|
||||
this.deadControlMetric(a, now, seen, 'alarm')
|
||||
this.close(ws, RELAY_CLOSE_CODE.PEER_DROPPED, 'control silence timeout')
|
||||
await this.onSocketGone(ws, RELAY_CLOSE_CODE.PEER_DROPPED, '')
|
||||
continue
|
||||
}
|
||||
// Same order as the cells' heartbeat: expired auth past its grace, then the 6 h lease.
|
||||
if (now > a.claims.exp + CONTROL_CONTINUITY_LIMITS.expiredAuthExistingSpliceGraceMs) {
|
||||
// A host already silent past the limit is gone (asleep, offline): it closes as one, so
|
||||
// drop counts stay comparable with the cells, whose pings would have closed it earlier.
|
||||
// Its probe is cut short: the expired authorization ends the socket either way.
|
||||
if (now - seen > this.config.silenceTimeoutMs) {
|
||||
this.deadControlMetric(a, now, seen, 'expired')
|
||||
this.close(ws, RELAY_CLOSE_CODE.PEER_DROPPED, 'control silence timeout')
|
||||
await this.onSocketGone(ws, RELAY_CLOSE_CODE.PEER_DROPPED, '', Math.min(now, seen + this.config.silenceTimeoutMs + this.config.pingIntervalMs))
|
||||
continue
|
||||
}
|
||||
const sinceRefreshS = a.lastRefreshAt === undefined ? -1 : Math.round((now - a.lastRefreshAt) / 1000)
|
||||
metric(this.env, 'auth_expired_close', sinceRefreshS, a.lastRefreshAt === undefined ? 'never' : 'refreshed', [], '', a.place)
|
||||
const seenAgoS = Math.round((now - seen) / 1000)
|
||||
metric(this.env, 'auth_expired_close', sinceRefreshS, a.lastRefreshAt === undefined ? 'never' : 'refreshed', [seenAgoS, a.activeAt === undefined ? -1 : Math.round((now - a.activeAt) / 1000)], this.keepalive(a) ? 'keepalive' : 'legacy', a.place)
|
||||
this.close(ws, RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'relay authorization expired')
|
||||
await this.onSocketGone(ws, RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, '')
|
||||
continue
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import type { Env } from './env'
|
||||
|
||||
export type MetricEvent =
|
||||
// label 'new' | 'rebind'; reason 'keepalive' | 'legacy' (took the keepalive offer or not);
|
||||
// double2 = ms from the upgrade to activation (the desktop gives up a connect after 15 s).
|
||||
| 'control_active'
|
||||
| 'control_close'
|
||||
| 'phone_open'
|
||||
@@ -26,7 +28,11 @@ export type MetricEvent =
|
||||
| 'control_upgrade'
|
||||
// Control closed 4401 for expired authorization: value = seconds since its last extending
|
||||
// refresh, label 'refreshed'; -1 and label 'never' when this socket never extended its token.
|
||||
// double2 = seconds since the host's last inbound frame or keepalive, double3 = seconds since the
|
||||
// control became active (-1 if unknown); reason 'keepalive' | 'legacy'.
|
||||
| 'auth_expired_close'
|
||||
// Control closed for silence (deadControlMetric, host-relay-control.ts).
|
||||
| 'silence_close'
|
||||
// Pre-auth socket refused by the per-source limiter: label 'phone' | 'data' | 'health'. Unpushed-host phones: 'not_on_edge'.
|
||||
| 'preauth_limited'
|
||||
| 'not_on_edge'
|
||||
|
||||
@@ -47,14 +47,14 @@ export function newHost(): Host {
|
||||
}
|
||||
}
|
||||
|
||||
export async function mint(key: KeyLike, relayHostId: string, issuer = ISSUER): Promise<string> {
|
||||
export async function mint(key: KeyLike, relayHostId: string, issuer = ISSUER, expiresIn = '5m'): Promise<string> {
|
||||
return new SignJWT({ purpose: 'host-control', prof: `prof-${USER}`, relayHostId })
|
||||
.setProtectedHeader({ alg: 'ES256', kid: REMOTE ? 'spike' : 'mvp' })
|
||||
.setSubject(USER)
|
||||
.setIssuer(issuer)
|
||||
.setAudience('orca-relay')
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('5m')
|
||||
.setExpirationTime(expiresIn)
|
||||
.sign(key)
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,32 @@ export async function keepaliveProbeCheck(key: KeyLike): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// K4: a keepalive host gone silent when its authorization expires closes as a dropped host (4408),
|
||||
// not 4401, so edge drop counts stay comparable with the cells (their pings would have closed it).
|
||||
export async function keepaliveExpiredSilentCheck(key: KeyLike): Promise<void> {
|
||||
const host = newHost()
|
||||
await assign(host.relayHostId, 'edge', EPOCH, [])
|
||||
const started = Date.now()
|
||||
// Alive for 50 s, then silent: the silence probe (sent ~9 s later) is still pending when the
|
||||
// 3 s token passes its 60 s grace at ~63 s, so the expiry check is what closes it.
|
||||
const c = control(host, await mint(key, host.relayHostId, undefined, '3s'), EPOCH, {
|
||||
caps: `${SHIPPED_CAPS},client-keepalive-v1`,
|
||||
drop: () => Date.now() - started > 50_000
|
||||
})
|
||||
await c.client.connect()
|
||||
try {
|
||||
const closed = await waitFor(() => c.events.closes.length > 0, 3_000 + 60_000 + 15_000)
|
||||
const after = Date.now() - started
|
||||
record(
|
||||
'K4 silent keepalive host at token expiry: closed 4408 (not 4401)',
|
||||
closed && c.events.closes.includes(4408) && !c.events.closes.includes(4401) && after >= 59_000,
|
||||
`closedAfter=${after}ms pings=${c.events.pings} closes=${c.events.closes}`
|
||||
)
|
||||
} finally {
|
||||
c.client.closeNow()
|
||||
}
|
||||
}
|
||||
|
||||
// F5: POST /__admin/drain sends the host's controls back to the director (drain frame, 4503).
|
||||
export async function drainCheck(key: KeyLike): Promise<void> {
|
||||
const host = newHost()
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
waitFor,
|
||||
type Host
|
||||
} from './harness-mvp-object'
|
||||
import { drainCheck, heartbeatCheck, keepaliveCheck, keepaliveProbeCheck, objectHealthCheck } from './mvp-object-hardening'
|
||||
import { drainCheck, heartbeatCheck, keepaliveCheck, keepaliveExpiredSilentCheck, keepaliveProbeCheck, objectHealthCheck } from './mvp-object-hardening'
|
||||
import { replayCheck, rotationCheck } from './mvp-object-replay'
|
||||
|
||||
async function mainRemote(): Promise<void> {
|
||||
@@ -107,7 +107,7 @@ async function main(): Promise<void> {
|
||||
await sleep(500)
|
||||
}
|
||||
await run(host, privateKey, () => jwksHits)
|
||||
await Promise.all([heartbeatCheck(privateKey), keepaliveCheck(privateKey), keepaliveProbeCheck(privateKey), drainCheck(privateKey), replayCheck(), rotationCheck()])
|
||||
await Promise.all([heartbeatCheck(privateKey), keepaliveCheck(privateKey), keepaliveProbeCheck(privateKey), keepaliveExpiredSilentCheck(privateKey), drainCheck(privateKey), replayCheck(), rotationCheck()])
|
||||
await sweepCheck()
|
||||
await preAuthCheck(stateDir)
|
||||
await objectHealthCheck(stateDir)
|
||||
|
||||
@@ -50,18 +50,18 @@ export function edgeCohortBucketPermille(relayHostId: string): number {
|
||||
}
|
||||
|
||||
// Mode off or down empties the cohort. Otherwise deny wins, then host allow, then region scope;
|
||||
// within the region, account allow or the rollout share.
|
||||
// within the region, account allow or the rollout share. Region 'any' skips the region scope.
|
||||
export function inEdgeCohort(
|
||||
config: EdgeRelayConfig,
|
||||
relayHostId: string,
|
||||
region: RelayRegion | undefined,
|
||||
region: RelayRegion | undefined | 'any',
|
||||
userId?: string
|
||||
): boolean {
|
||||
const flags = config.control?.() ?? EDGE_CONTROL_DEFAULTS
|
||||
if (flags.mode !== 'on') { return false }
|
||||
if (config.denyHosts.has(relayHostId)) { return false }
|
||||
if (config.allowHosts.has(relayHostId)) { return true }
|
||||
if (!config.regions.has(region ?? RELAY_DEFAULT_REGION)) { return false }
|
||||
if (region !== 'any' && !config.regions.has(region ?? RELAY_DEFAULT_REGION)) { return false }
|
||||
if (userId !== undefined && config.allowUsers?.has(userId)) { return true }
|
||||
return edgeCohortBucketPermille(relayHostId) < edgeRolloutPermille(config.rolloutPercent, flags)
|
||||
}
|
||||
|
||||
@@ -108,6 +108,22 @@ describe('edge evacuation under runtime control', () => {
|
||||
h.evacuation.stop()
|
||||
})
|
||||
|
||||
it('leaves region scope to the assign path: a stored preference outside it drains nobody', async () => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const flags: EdgeControlFlags = { mode: 'on', rolloutPermille: 1000 }
|
||||
const h = await harness({ control: () => flags })
|
||||
// On the edge by its ask (no region, or us-central1); its stored preference says otherwise.
|
||||
await h.insert(hostId(1))
|
||||
await h.prefer(hostId(1), 'asia-east2')
|
||||
await h.insert(hostId(2))
|
||||
expect(await h.evacuation.sweep()).toBe(0)
|
||||
expect(h.admin.drained).toEqual([])
|
||||
// Leaving the cohort still drains, whatever the stored region.
|
||||
flags.rolloutPermille = 0
|
||||
expect(await h.evacuation.sweep()).toBe(2)
|
||||
expect(h.admin.drained.sort()).toEqual([hostId(1), hostId(2)])
|
||||
})
|
||||
|
||||
it(`sends at most ${EDGE_EVACUATION_CONCURRENCY} drains at once`, async () => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const h = await harness()
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// rows whose host is no longer in the cohort and asks the edge to drain them; the desktop then
|
||||
// re-asks /v1/assign, and that ask exports its data and places it on a cell (app.ts). Each pass also
|
||||
// retries fences that did not land after a host left (edge-relay-fence.ts).
|
||||
import { RELAY_DEFAULT_REGION, RelayRegionSchema } from '@orca-cloud/relay-contract/relay-regions'
|
||||
import type { RelayDatabase } from './database/database.js'
|
||||
import { EdgeAdminUnavailableError, type EdgeAdmin } from './edge-admin-client.js'
|
||||
import { inEdgeCohort, text, type EdgeRelayConfig, type Identity } from './edge-relay-cohort.js'
|
||||
@@ -116,9 +115,11 @@ export class EdgeEvacuation {
|
||||
for (const row of rows) {
|
||||
const identity = { userId: text(row, 'user_id'), relayHostId: text(row, 'relay_host_id') }
|
||||
pass.cursor = identity
|
||||
const stored = RelayRegionSchema.safeParse(row.preferred_region)
|
||||
const region = stored.success ? stored.data : RELAY_DEFAULT_REGION
|
||||
if (inEdgeCohort(this.config, identity.relayHostId, region, identity.userId)) { continue }
|
||||
// Region scope is the assign path's call alone: it decides with the region the desktop
|
||||
// asks for, which the stored preference can miss or lag. Judging it here too drained
|
||||
// hosts that the next ask put straight back (a drain every few minutes). A host that asks
|
||||
// for a region outside the scope leaves on that ask.
|
||||
if (inEdgeCohort(this.config, identity.relayHostId, 'any', identity.userId)) { continue }
|
||||
if (this.drainedAt.has(drainKey(identity))) { continue }
|
||||
leaving.push(identity)
|
||||
// Resume after the last host taken, so the rest of the page is read next tick.
|
||||
@@ -143,10 +144,8 @@ export class EdgeEvacuation {
|
||||
private async readPage(pass: Pass) {
|
||||
const { cursor, origin, wrapped } = pass
|
||||
return await this.database.query(
|
||||
`SELECT a.user_id, a.relay_host_id, p.preferred_region
|
||||
`SELECT a.user_id, a.relay_host_id
|
||||
FROM relay_assignments a
|
||||
LEFT JOIN relay_assignment_region_preferences p
|
||||
ON p.user_id = a.user_id AND p.relay_host_id = a.relay_host_id
|
||||
WHERE a.cell_id = ? AND (a.user_id > ? OR (a.user_id = ? AND a.relay_host_id > ?))
|
||||
${wrapped ? 'AND (a.user_id < ? OR (a.user_id = ? AND a.relay_host_id <= ?))' : ''}
|
||||
ORDER BY a.user_id, a.relay_host_id
|
||||
|
||||
@@ -116,6 +116,43 @@ describe('relay edge fence after a host leaves', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('relay edge fence over a same-epoch conflict', () => {
|
||||
it('fences once more one epoch above a conflict at its epoch, and never bumps over a stale answer', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const h = await harness()
|
||||
h.edge.answers = ['conflict', 'applied']
|
||||
expect(await h.assignments.fenceAway({ userId: USER, relayHostId: HOST }, 'us-c1', 9)).toBe(true)
|
||||
expect(h.edge.calls.map((call) => call.body.assignmentEpoch)).toEqual([9, 10])
|
||||
expect(h.edge.calls.every((call) => call.body.cellId === 'us-c1')).toBe(true)
|
||||
expect(logged(warn, 'orca_relay_edge_fence_above_conflict')).toEqual([
|
||||
{ event: 'orca_relay_edge_fence_above_conflict', outcome: 'applied', assignmentEpoch: 10 }
|
||||
])
|
||||
expect(await h.pending()).toEqual([])
|
||||
// Stale: the edge holds a newer epoch (the host may be back there); no bump, recorded for retry.
|
||||
h.edge.calls = []
|
||||
h.edge.answers = ['stale']
|
||||
expect(await h.assignments.fenceAway({ userId: USER, relayHostId: HOST }, 'us-c1', 11)).toBe(false)
|
||||
expect(h.edge.calls.map((call) => call.body.assignmentEpoch)).toEqual([11])
|
||||
})
|
||||
|
||||
it('a pending fence retried into a same-epoch conflict lands one epoch above the row', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const h = await harness()
|
||||
await h.placeRow('us-c1', 7)
|
||||
h.edge.answers = ['down', 'down', 'down', 'down', 'down', 'down', 'down', 'down', 'down', 'down']
|
||||
expect(await h.assignments.fenceAway({ userId: USER, relayHostId: HOST }, 'us-c1', 7)).toBe(false)
|
||||
expect(await h.pending()).toHaveLength(1)
|
||||
h.edge.calls = []
|
||||
h.edge.answers = ['conflict', 'applied']
|
||||
h.clock.now += 60_000
|
||||
await h.retry()
|
||||
expect(h.edge.calls.map((call) => call.body.assignmentEpoch)).toEqual([7, 8])
|
||||
expect(await h.pending()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('relay edge pending fence retries', () => {
|
||||
it('re-fences to the current row, not the recorded one, and clears the record once it lands', async () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
@@ -41,6 +41,27 @@ export async function recordPendingFence(
|
||||
)
|
||||
}
|
||||
|
||||
// One fence call. `conflict` means the edge holds this very epoch for another placement (a move onto
|
||||
// the edge whose push landed though the director counted it as failed, then a placement elsewhere
|
||||
// minted the same epoch). No later move can produce that (a later move has a higher epoch, which
|
||||
// answers `stale`), so the fence goes once more one epoch above: it lands, and a later move onto
|
||||
// the edge re-mints above it as for any edge epoch (moveToEdge).
|
||||
async function assignFence(
|
||||
admin: EdgeAdmin,
|
||||
fence: Identity & { cellId: string; assignmentEpoch: number }
|
||||
): Promise<{ outcome: string; assignmentEpoch: number }> {
|
||||
const first = await admin.assign(fence)
|
||||
if (first.outcome !== 'conflict' || first.assignmentEpoch !== fence.assignmentEpoch) {
|
||||
return { outcome: first.outcome, assignmentEpoch: fence.assignmentEpoch }
|
||||
}
|
||||
const above = fence.assignmentEpoch + 1
|
||||
const second = await admin.assign({ ...fence, assignmentEpoch: above })
|
||||
console.warn(
|
||||
JSON.stringify({ event: 'orca_relay_edge_fence_above_conflict', outcome: second.outcome, assignmentEpoch: above })
|
||||
)
|
||||
return { outcome: second.outcome, assignmentEpoch: above }
|
||||
}
|
||||
|
||||
// The fence right after a host left (EdgeAssignments.fenceAway). Anything but applied/unchanged
|
||||
// fails: unavailable, `conflict`, or `stale` (the edge holds a newer epoch, so it still claims the
|
||||
// host); the failure is logged and recorded for retry. Returns whether the fence landed.
|
||||
@@ -55,7 +76,7 @@ export async function fenceEdge(
|
||||
let outcome = 'unavailable'
|
||||
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
||||
try {
|
||||
outcome = (await admin.assign(fence)).outcome
|
||||
outcome = (await assignFence(admin, fence)).outcome
|
||||
} catch (error) {
|
||||
if (!(error instanceof EdgeAdminUnavailableError)) { throw error }
|
||||
outcome = error.message
|
||||
@@ -138,7 +159,7 @@ async function retryOne(
|
||||
const assignmentEpoch = integer(row, 'assignment_epoch')
|
||||
let outcome: string
|
||||
try {
|
||||
outcome = (await admin.assign({ ...identity, cellId, assignmentEpoch })).outcome
|
||||
outcome = (await assignFence(admin, { ...identity, cellId, assignmentEpoch })).outcome
|
||||
} catch (error) {
|
||||
if (!(error instanceof EdgeAdminUnavailableError)) { throw error }
|
||||
outcome = error.message
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
id: profile-full-save-callers
|
||||
valid:
|
||||
- "scheduleSave(scheduling, ['repos'])"
|
||||
invalid:
|
||||
- 'scheduleFullProfileSave(scheduling)'
|
||||
@@ -0,0 +1,14 @@
|
||||
# Mutations save the domains they change; only startup load and maintenance resume rewrite the whole profile.
|
||||
id: profile-full-save-callers
|
||||
language: typescript
|
||||
severity: error
|
||||
message: Name the profile domains this mutation changed with scheduleSave(owner, [...domains]). scheduleFullProfileSave is for startup load and maintenance resume only.
|
||||
files:
|
||||
- src/**
|
||||
ignores:
|
||||
- '**/*.test.*'
|
||||
- src/main/persistence/loading-store/write-scheduling.ts
|
||||
- src/main/persistence/loading-store/store.ts
|
||||
- src/main/persistence/loading-store/profile-state-maintenance.ts
|
||||
rule:
|
||||
pattern: scheduleFullProfileSave($$$)
|
||||
@@ -29,7 +29,6 @@
|
||||
1 test-no-in-test-import-actual:src/renderer/src/components/terminal-pane/pty-connection/notifications/pty-connection-hook-idle-arbitration.test.ts
|
||||
3 test-no-in-test-import-actual:src/renderer/src/components/terminal-pane/snapshots/remote-snapshot-alt-screen-host-chain.test.ts
|
||||
1 test-no-in-test-import-actual:src/renderer/src/hooks/useIpcEvents-agent-status-pane-teardown.test.ts
|
||||
1 test-no-in-test-import-actual:src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-hydration.test.ts
|
||||
1 test-no-polling-loop:src/main/browser/cookies/browser-cookie-import-concurrency.test.ts
|
||||
1 test-no-polling-loop:src/main/claude/claude-stop-ack-race.test.ts
|
||||
1 test-no-polling-loop:src/main/claude/claude-stream-json-connection.test.ts
|
||||
@@ -264,8 +263,7 @@
|
||||
1 test-no-real-time-wait:src/renderer/src/components/terminal-pane/layout/split/resolve-split-cwd.test.ts
|
||||
2 test-no-real-time-wait:src/renderer/src/components/terminal-pane/pty-connection/emulation/fish-color-scheme-child-stdin.node-pty.test.ts
|
||||
1 test-no-real-time-wait:src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-connect-failure-recovery.test.ts
|
||||
2 test-no-real-time-wait:src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-host-exit-record.test.ts
|
||||
3 test-no-real-time-wait:src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-subscribe-ack-input.test.ts
|
||||
1 test-no-real-time-wait:src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-subscribe-ack-input.test.ts
|
||||
1 test-no-real-time-wait:src/renderer/src/components/terminal-pane/viewport/terminal-fit-restore.test.ts
|
||||
1 test-no-real-time-wait:src/renderer/src/hooks/runtime-project-refresh-scheduler.test.ts
|
||||
1 test-no-real-time-wait:src/renderer/src/lib/agent-session/resume/cli-fork/native-chat-cli-fork-preparation.test.ts
|
||||
@@ -626,12 +624,7 @@
|
||||
2 test-no-wall-clock:src/renderer/src/components/artifacts/ArtifactCollection.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.adoption-chrome.test.tsx
|
||||
2 test-no-wall-clock:src/renderer/src/components/dashboard/useLiveDashboardSnapshot.test.ts
|
||||
1 test-no-wall-clock:src/renderer/src/components/native-chat/conversation/slots/NativeChatMessageList.opening-send.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/native-chat/conversation/slots/NativeChatMessageList.queued-turn-order.test.tsx
|
||||
2 test-no-wall-clock:src/renderer/src/components/native-chat/conversation/turns/NativeChatMessageList.turn-indicator.test.tsx
|
||||
3 test-no-wall-clock:src/renderer/src/components/native-chat/conversation/turns/NativeChatResolvedView.turn-status.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/native-chat/session/structured/delivery/NativeChatMessageList.unsent-message.test.tsx
|
||||
3 test-no-wall-clock:src/renderer/src/components/native-chat/surfaces/integration/NativeChatMessageList.test.tsx
|
||||
2 test-no-wall-clock:src/renderer/src/components/native-chat/tools/tasks/NativeChatStructuredSessionStatus.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/sidebar/workspace-card/review/use-workspace-reference-details.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/sidebar/worktree-list/listing/WorktreeList.lineage-agent-expansion-coupling.test.tsx
|
||||
@@ -641,18 +634,12 @@
|
||||
1 test-no-wall-clock:src/renderer/src/components/terminal/initial-terminal-structured-launch.test.tsx
|
||||
2 test-no-wall-clock:src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-removal.agent-stop.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/worktree-jump/WorktreeJumpPalette.recent-tabs.behavior.test.tsx
|
||||
4 test-no-wall-clock:src/renderer/src/components/worktree-jump/WorktreeJumpPalette.recent-tabs.test.tsx
|
||||
2 test-no-wall-clock:src/renderer/src/hooks/ipc-events/agent-status-terminal-title-tab-write.test.ts
|
||||
1 test-no-wall-clock:src/renderer/src/lib/paired-runtime-closed-tab-resume.test.ts
|
||||
2 test-no-wall-clock:src/renderer/src/lib/use-tab-agent-observed-signal-dispatch.test.tsx
|
||||
2 test-no-wall-clock:src/renderer/src/lib/wake-sleeping-agents-live-unmounted-pane.test.ts
|
||||
2 test-no-wall-clock:src/renderer/src/lib/agent-session/tab-identity/use-tab-agent-observed-signal-dispatch.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/lib/workspace/ownership/workspace-terminal-host-authority.test.ts
|
||||
1 test-no-wall-clock:src/renderer/src/lib/workspace/activation/worktree-activation-emptied-workspace-reseed.test.ts
|
||||
2 test-no-wall-clock:src/renderer/src/store/github/pull-requests/github-pr-branch-linked-pr-divergence.test.ts
|
||||
6 test-no-wall-clock:src/renderer/src/store/slices/jira/jira.test.ts
|
||||
5 test-no-wall-clock:src/renderer/src/store/slices/worktrees/metadata/worktree-meta-hosted-review.test.ts
|
||||
1 test-no-wall-clock:src/renderer/src/store/slices/worktrees/session/worktrees-terminal-reference-origins.test.ts
|
||||
1 test-no-wall-clock:src/renderer/src/store/terminals/store-sleep-exact-runtime-stop.test.ts
|
||||
1 test-no-wall-clock:src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts
|
||||
7 test-no-wall-clock:src/shared/__tests__/process-host/windows-tree-kill-late-taskkill.win32.test.ts
|
||||
1 test-no-wall-clock:src/shared/agent-hook-listener/transport/agent-hook-spool-drain.test.ts
|
||||
|
||||
@@ -220,20 +220,20 @@
|
||||
1 src/renderer/src/lib/browser/navigation/http-link-localhost-route.ts # extracted from http-link-routing.ts
|
||||
1 src/renderer/src/lib/browser/navigation/http-link-routing.ts
|
||||
2 src/renderer/src/lib/browser/workspace/workspace-browser-tab-open.ts
|
||||
1 src/renderer/src/lib/client-creation-action-error.ts
|
||||
2 src/renderer/src/lib/client-creation-action-policy.ts
|
||||
1 src/renderer/src/lib/workspace/creation/surface-policy/client-creation-action-error.ts
|
||||
2 src/renderer/src/lib/workspace/creation/surface-policy/client-creation-action-policy.ts
|
||||
1 src/renderer/src/runtime/hosts/selection/default-creation-host.ts # the sanctioned reader of the default host
|
||||
1 src/renderer/src/lib/launch-ai-vault-session.ts
|
||||
1 src/renderer/src/lib/local-preflight-context.ts
|
||||
1 src/renderer/src/lib/agent-session/resume/history/launch-ai-vault-session.ts
|
||||
1 src/renderer/src/runtime/hosts/preflight/local-preflight-context.ts
|
||||
2 src/renderer/src/runtime/hosts/observations/provider-runtime-context.ts
|
||||
2 src/renderer/src/lib/workspace/ownership/repo-runtime-owner.ts
|
||||
1 src/renderer/src/lib/workspace/ownership/runtime-session-mirror-owners.ts
|
||||
1 src/renderer/src/lib/runtime-session-mirror-targets.ts
|
||||
1 src/renderer/src/runtime/web-session-tabs-sync/subscriptions/runtime-session-mirror-targets.ts
|
||||
1 src/renderer/src/lib/workspace/ownership/single-runtime-legacy-owner.ts
|
||||
1 src/renderer/src/lib/skills/runtime/skill-runtime-command.ts
|
||||
1 src/renderer/src/lib/workspace/ownership/terminal-close-route.ts
|
||||
4 src/renderer/src/lib/workspace/ownership/terminal-worktree-route.ts # resolveTerminalPtyOwner: teardown, close and PTY identity read spawn's attribution (round 3 F8), which retired the deprecated resolver's readers
|
||||
3 src/renderer/src/lib/unread-badge-count-selector.ts
|
||||
3 src/renderer/src/attention/unread/unread-badge-count-selector.ts
|
||||
1 src/renderer/src/runtime/hosted-terminals/restoration/web-runtime-worktree-terminal-after-wake.ts
|
||||
1 src/renderer/src/lib/work-items/linear/linear-issue-url-lookup.ts
|
||||
1 src/renderer/src/lib/work-items/linear/linear-issue-write.ts
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
10 src/main/persistence/loading-store/repo-lifecycle-operations.ts
|
||||
10 src/main/persistence/loading-store/session-host-partitions.ts
|
||||
9 src/main/persistence/loading-store/session-snapshot-operations.ts
|
||||
3 src/main/persistence/loading-store/state-serialization-secret-handling.ts
|
||||
2 src/main/persistence/loading-store/state-serialization-secret-handling.ts
|
||||
4 src/main/persistence/loading-store/workspace-session-snapshot-publication.ts
|
||||
4 src/main/persistence/loading-store/workspace-session-terminal-binding-replay.ts
|
||||
2 src/main/persistence/migrating-orcad-catalog/orcad-destination-client-state.ts
|
||||
|
||||
@@ -482,7 +482,7 @@
|
||||
2 src/main/runtime/mobile-cloud-relay/relay-revoke-outbox.ts
|
||||
1 src/main/runtime/orchestration/db/runtime-orchestration-database-access.ts
|
||||
2 src/main/runtime/push/push-unregister-outbox.ts
|
||||
2 src/main/runtime/rpc/methods/mobile-delivery/bundled-mobile-web-bundle.ts
|
||||
1 src/main/runtime/rpc/methods/mobile-delivery/bundled-mobile-web-bundle.ts
|
||||
1 src/main/runtime/rpc/server/network-source-policy-store.ts
|
||||
2 src/main/runtime/rpc/server/pairing/device-registry-corruption.ts
|
||||
2 src/main/runtime/rpc/server/pairing/device-registry-disk-merge.ts
|
||||
|
||||
@@ -42,70 +42,70 @@
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "mobile/src/components/accounts-snapshot.ts",
|
||||
"filePath": "mobile/src/accounts/accounts-snapshot.ts",
|
||||
"kind": "object-property:message",
|
||||
"text": "Claude limits use the wrong provider identity",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "mobile/src/components/accounts-snapshot.ts",
|
||||
"filePath": "mobile/src/accounts/accounts-snapshot.ts",
|
||||
"kind": "object-property:message",
|
||||
"text": "Codex limits use the wrong provider identity",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "mobile/src/components/accounts-snapshot.ts",
|
||||
"filePath": "mobile/src/accounts/accounts-snapshot.ts",
|
||||
"kind": "object-property:message",
|
||||
"text": "Host Codex accounts cannot name a WSL distro",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "mobile/src/components/accounts-snapshot.ts",
|
||||
"filePath": "mobile/src/accounts/accounts-snapshot.ts",
|
||||
"kind": "object-property:message",
|
||||
"text": "Host rate-limit targets cannot name a WSL distro",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "mobile/src/components/accounts-snapshot.ts",
|
||||
"filePath": "mobile/src/accounts/accounts-snapshot.ts",
|
||||
"kind": "object-property:message",
|
||||
"text": "Inactive Claude limits use the wrong provider identity",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "mobile/src/components/accounts-snapshot.ts",
|
||||
"filePath": "mobile/src/accounts/accounts-snapshot.ts",
|
||||
"kind": "object-property:message",
|
||||
"text": "Inactive Codex limits use the wrong provider identity",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "mobile/src/components/accounts-snapshot.ts",
|
||||
"filePath": "mobile/src/accounts/accounts-snapshot.ts",
|
||||
"kind": "object-property:message",
|
||||
"text": "WSL Codex accounts require an exact distro",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "mobile/src/components/accounts-snapshot.ts",
|
||||
"filePath": "mobile/src/accounts/accounts-snapshot.ts",
|
||||
"kind": "object-property:message",
|
||||
"text": "WSL rate-limit targets require an exact distro",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "mobile/src/components/codex-reset-credit-reply-schema.ts",
|
||||
"filePath": "mobile/src/accounts/usage/codex-reset-credit-reply-schema.ts",
|
||||
"kind": "object-property:message",
|
||||
"text": "Host reset scopes cannot name a WSL distro",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "mobile/src/components/codex-reset-credit-reply-schema.ts",
|
||||
"filePath": "mobile/src/accounts/usage/codex-reset-credit-reply-schema.ts",
|
||||
"kind": "object-property:message",
|
||||
"text": "WSL reset scopes require an exact distro",
|
||||
"dynamic": false,
|
||||
|
||||
+386
-673
File diff suppressed because one or more lines are too long
@@ -18,7 +18,7 @@
|
||||
3 src/renderer/src/components/worktree-jump/worktree-jump-palette-status-inputs.ts
|
||||
1 src/renderer/src/lib/agent-session/hibernation/agent-hibernation-coordinator.ts
|
||||
4 src/renderer/src/components/floating-terminal/floating-workspace-terminal-actions.ts
|
||||
1 src/renderer/src/lib/pane-manager/client-hosted-browser-row-state.ts
|
||||
1 src/renderer/src/runtime/browser/client-hosting/client-hosted-browser-row-state.ts
|
||||
4 src/renderer/src/lib/workspace/persistence/session-write-subscriber.ts
|
||||
1 src/renderer/src/lib/workspace/persistence/workspace-session-browser-tabs.ts
|
||||
7 src/renderer/src/lib/workspace/persistence/workspace-session-host-hydration.ts
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,9 @@ export const CJK_LATIN_SPACED_TERMS = [
|
||||
'Kimi',
|
||||
'OpenCode',
|
||||
'Orca',
|
||||
'Artifacts',
|
||||
'Artifact',
|
||||
'Relay',
|
||||
'Cursor',
|
||||
'Bitbucket',
|
||||
'Tailscale',
|
||||
|
||||
@@ -152,7 +152,7 @@ export const JA_UNIFIED_VALUES = {
|
||||
'The GitLab CLI is installed but not authenticated. Run this command in a terminal:':
|
||||
'GitLab CLI はインストールされていますが、認証されていません。ターミナルで次のコマンドを実行してください:',
|
||||
'Install GitLab CLI': 'GitLab CLI をインストール',
|
||||
'CLI.': 'CLI。',
|
||||
'CLI.': 'CLI',
|
||||
'The GitHub CLI is installed but not authenticated. Run this command in a terminal:':
|
||||
'GitHub CLI はインストールされていますが、認証されていません。ターミナルで次のコマンドを実行してください:',
|
||||
'Install GitHub CLI': 'GitHub CLI をインストール',
|
||||
|
||||
@@ -116,7 +116,7 @@ const BASE_LOCALE_KEY_OVERRIDES = {
|
||||
},
|
||||
'auto.components.settings.SettingsSidebar.82db1b7de4': {
|
||||
ko: '온보딩 체크리스트, {{value0}}/{{value1}} 완료. 설정 가이드 보기.',
|
||||
zh: '入门清单,已完成 {{value0}}/{{value1}}。显示设置指南。',
|
||||
zh: '入门清单,已完成 {{value0}}/{{value1}}。显示设置指南',
|
||||
ja: 'オンボーディングチェックリスト、{{value0}}/{{value1}} 完了。セットアップガイドを表示。'
|
||||
},
|
||||
'auto.components.settings.ShortcutFilterRail.02dc7d4251': {
|
||||
@@ -307,12 +307,12 @@ const BASE_LOCALE_KEY_OVERRIDES = {
|
||||
},
|
||||
'settings.appearance.statusBar.kimiToggleDescription': {
|
||||
ko: '활성 워크스페이스의 Kimi 구독 사용량을 표시합니다.',
|
||||
zh: '显示当前工作区的 Kimi 订阅使用情况。',
|
||||
zh: '显示当前工作区的 Kimi 订阅使用情况',
|
||||
ja: 'アクティブなワークスペースの Kimi サブスクリプション使用状況を表示します。'
|
||||
},
|
||||
'auto.components.mobile.MobileHero.cd4e5e816f': {
|
||||
ko: '주머니 속의 워크스페이스.',
|
||||
zh: '您的工作区就在您的口袋里。',
|
||||
zh: '您的工作区就在您的口袋里',
|
||||
ja: 'ワークスペースをポケットに。'
|
||||
},
|
||||
'auto.components.GitHubItemDialog.dbe5e2448e': {
|
||||
@@ -332,12 +332,12 @@ const BASE_LOCALE_KEY_OVERRIDES = {
|
||||
},
|
||||
'auto.components.JiraIssueWorkspace.857bd2f88f': {
|
||||
ko: '선택한 이슈를 미리보기하고, 편집하고, 작업을 시작하세요.',
|
||||
zh: '预览、编辑并从选定的议题开始工作。',
|
||||
zh: '预览、编辑并从选定的议题开始工作',
|
||||
ja: '選択したイシューをプレビュー・編集し、そこから作業を開始します。'
|
||||
},
|
||||
'auto.components.LinearIssueWorkspace.ad5dec37b7': {
|
||||
ko: '선택한 이슈를 미리보기하고, 편집하고, 작업을 시작하세요.',
|
||||
zh: '预览、编辑并从选定的议题开始工作。',
|
||||
zh: '预览、编辑并从选定的议题开始工作',
|
||||
ja: '選択したイシューをプレビュー・編集し、そこから作業を開始します。'
|
||||
},
|
||||
'auto.components.TaskPage.67662ade50': {
|
||||
@@ -352,12 +352,12 @@ const BASE_LOCALE_KEY_OVERRIDES = {
|
||||
},
|
||||
'auto.components.TaskPage.1e1b2ad8f2': {
|
||||
ko: '이 이슈를 등록하기 전에 프로젝트 워크스페이스에서 팀을 선택하세요.',
|
||||
zh: '在提交此议题之前,请从项目工作区选择一个团队。',
|
||||
zh: '在提交此议题之前,请从项目工作区选择一个团队',
|
||||
ja: 'このイシューを登録する前に、プロジェクトワークスペースからチームを選択してください。'
|
||||
},
|
||||
'auto.components.TaskPage.94d900518d': {
|
||||
ko: '선택한 프리셋과 일치하는 이슈가 없습니다.',
|
||||
zh: '没有与所选预设匹配的议题。',
|
||||
zh: '没有与所选预设匹配的议题',
|
||||
ja: '選択したプリセットに一致するイシューはありません。'
|
||||
},
|
||||
'auto.components.linear.project.view.surfaces.3ad562bdf4': {
|
||||
@@ -392,7 +392,7 @@ const BASE_LOCALE_KEY_OVERRIDES = {
|
||||
},
|
||||
'auto.components.onboarding.OnboardingFlow.04ae28d8ca': {
|
||||
ko: '몇 시간 내내 보고 싶은 테마를 선택하세요.',
|
||||
zh: '选择你想盯着看几个小时的主题。',
|
||||
zh: '选择你想盯着看几个小时的主题',
|
||||
ja: '何時間も眺めていたくなるテーマを選んでください。'
|
||||
},
|
||||
'auto.components.GitLabItemDialog.e089f62594': {
|
||||
@@ -402,12 +402,12 @@ const BASE_LOCALE_KEY_OVERRIDES = {
|
||||
},
|
||||
'auto.components.right.sidebar.source.control.primary.action.3d5dccef0b': {
|
||||
ko: '커밋할 것이 없습니다. PR은 이미 병합되었습니다.',
|
||||
zh: '没有可提交的内容。PR 已合并。',
|
||||
zh: '没有可提交的内容。PR 已合并',
|
||||
ja: 'コミットするものはありません。PR はすでにマージされています。'
|
||||
},
|
||||
'auto.components.settings.integrations.search.16a486a49d': {
|
||||
ko: 'Linear에 연결해 이슈를 탐색하고 연결합니다.',
|
||||
zh: '连接 Linear 以浏览和链接议题。',
|
||||
zh: '连接 Linear 以浏览和链接议题',
|
||||
ja: 'Linear に接続してイシューを参照し、リンクします。'
|
||||
},
|
||||
'menu.showTitlebarAppName': {
|
||||
@@ -422,7 +422,7 @@ const BASE_LOCALE_KEY_OVERRIDES = {
|
||||
},
|
||||
'auto.lib.fix.checks.agent.launch.9f00d7df0c': {
|
||||
ko: '검사 프롬프트가 비어 있습니다. 소스 제어 AI 설정을 업데이트하세요.',
|
||||
zh: '检查提示为空。请更新源代码管理 AI 设置。',
|
||||
zh: '检查提示为空。请更新源代码管理 AI 设置',
|
||||
ja: 'チェック プロンプトが空です。ソース管理 AI 設定を更新してください。'
|
||||
},
|
||||
'auto.components.TaskPage.2af3ab5c58': {
|
||||
@@ -442,7 +442,7 @@ const BASE_LOCALE_KEY_OVERRIDES = {
|
||||
},
|
||||
'auto.components.TaskPage.25ff84769a': {
|
||||
ko: '이 Linear 컨텍스트와 일치하는 이슈가 없습니다.',
|
||||
zh: '没有议题与此 Linear 上下文匹配。',
|
||||
zh: '没有议题与此 Linear 上下文匹配',
|
||||
ja: 'この Linear コンテキストに一致するイシューはありません。'
|
||||
},
|
||||
'auto.components.GitHubItemDialog.8c45901789': {
|
||||
@@ -512,7 +512,7 @@ const BASE_LOCALE_KEY_OVERRIDES = {
|
||||
},
|
||||
'auto.components.GitHubItemDialog.a341343303': {
|
||||
ko: '리뷰 댓글이 추가되었습니다.',
|
||||
zh: '已添加评审评论。',
|
||||
zh: '已添加评审评论',
|
||||
ja: 'レビューコメントを追加しました。'
|
||||
},
|
||||
// Port forwarding "Forward" is 転送, not the browser-navigation 進む.
|
||||
|
||||
@@ -9,13 +9,13 @@ export const MACOS_TCC_KEY_OVERRIDES = {
|
||||
es: 'Los mensajes de permisos de macOS pueden aparecer cuando un agente o una herramienta de terminal que se ejecuta en Orca intenta acceder a archivos protegidos. Concede acceso total al disco en Ajustes para reducir estos avisos.',
|
||||
ja: 'Orca で実行中のエージェントやターミナルツールが保護されたファイルにアクセスしようとすると、macOS の権限メッセージが表示されることがあります。これらの確認を減らすには、設定でフルディスクアクセスを許可してください。',
|
||||
ko: 'Orca에서 실행 중인 에이전트나 터미널 도구가 보호된 파일에 접근하려고 하면 macOS 권한 메시지가 표시될 수 있습니다. 이러한 요청을 줄이려면 설정에서 전체 디스크 접근 권한을 허용하세요.',
|
||||
zh: '当 Orca 中运行的智能体或终端工具尝试访问受保护的文件时,macOS 可能会显示权限信息。请在“设置”中授予“完全磁盘访问权限”,以减少此类提示。'
|
||||
zh: '当 Orca 中运行的智能体或终端工具尝试访问受保护的文件时,macOS 可能会显示权限信息。请在“设置”中授予“完全磁盘访问权限”,以减少此类提示'
|
||||
},
|
||||
'auto.components.settings.DeveloperPermissionsPane.7ca17b62c8': {
|
||||
es: 'Cuando los agentes que ejecuta Orca leen datos de otras apps, macOS muestra el nombre de Orca porque es el proceso responsable de los comandos de terminal. Concede este permiso a Orca para reducir esos avisos. Después, cierra y vuelve a abrir Orca.',
|
||||
ja: 'Orca が実行するエージェントがほかのアプリのデータを読み取ると、ターミナルコマンドの実行元プロセスである Orca の名前が macOS に表示されます。これらの確認を減らすには、Orca にこの権限を許可してください。その後、Orca を終了して再度開いてください。',
|
||||
ko: 'Orca가 실행하는 에이전트가 다른 앱의 데이터를 읽으면, macOS는 터미널 명령을 실행하는 프로세스인 Orca를 표시합니다. 이러한 요청을 줄이려면 Orca에 이 권한을 허용하세요. 그런 다음 Orca를 종료했다가 다시 여세요.',
|
||||
zh: '当 Orca 运行的智能体读取其他应用的数据时,macOS 会显示 Orca,因为 Orca 是执行终端命令的进程。请为 Orca 授予此权限,以减少此类提示。然后退出并重新打开 Orca。'
|
||||
zh: '当 Orca 运行的智能体读取其他应用的数据时,macOS 会显示 Orca,因为 Orca 是执行终端命令的进程。请为 Orca 授予此权限,以减少此类提示。然后退出并重新打开 Orca'
|
||||
},
|
||||
'auto.components.settings.DeveloperPermissionsPane.c566bca278': {
|
||||
ko: '전체 디스크 접근 권한',
|
||||
|
||||
@@ -197,8 +197,8 @@ export const LOCALE_VALUE_OVERRIDES = {
|
||||
Next: '下一步',
|
||||
Beta: '测试版',
|
||||
Dismiss: '关闭',
|
||||
Artifacts: '工件',
|
||||
artifacts: '工件',
|
||||
Artifacts: '产物',
|
||||
artifacts: '产物',
|
||||
Native: '原生',
|
||||
Bundled: '内置',
|
||||
Quarter: '季度',
|
||||
|
||||
@@ -22,7 +22,7 @@ export const ZH_VALUE_OVERRIDES = {
|
||||
reviewer: '评审人',
|
||||
review: '评审',
|
||||
Review: '评审',
|
||||
'No matching reviewers.': '没有匹配的评审人。',
|
||||
'No matching reviewers.': '没有匹配的评审人',
|
||||
'Request up to 15 reviewers': '最多请求 15 名评审人',
|
||||
'No reviewers requested.': '未请求评审人',
|
||||
'Remove reviewer {{value0}}': '移除评审人 {{value0}}',
|
||||
@@ -39,8 +39,8 @@ export const ZH_VALUE_OVERRIDES = {
|
||||
'Enter a reviewer': '输入评审人',
|
||||
'Add reviewer': '添加评审人',
|
||||
'No reviewers.': '没有评审人',
|
||||
'Open the PR details to view current reviewers.': '打开 PR 详情以查看当前评审人。',
|
||||
'Review comment added.': '已添加评审评论。',
|
||||
'Open the PR details to view current reviewers.': '打开 PR 详情以查看当前评审人',
|
||||
'Review comment added.': '已添加评审评论',
|
||||
'Loading labels': '加载标签',
|
||||
// Why: MT rendered the "Pin Tab" action as "引脚标签" (noun reading of "pin"); pair it with 取消固定标签.
|
||||
'Pin Tab': '固定标签',
|
||||
@@ -57,12 +57,12 @@ export const ZH_VALUE_OVERRIDES = {
|
||||
'Markdown Review Notes': 'Markdown 评审笔记',
|
||||
'Reviewed by': '评审人',
|
||||
'Review from': '评审来自',
|
||||
'Reviewer id is unavailable for this GitLab user.': '此 GitLab 用户无法获取评审人 ID。',
|
||||
'Create a {{value0}} to start checks and review.': '创建 {{value0}} 以开始检查和评审。',
|
||||
'Reviewer id is unavailable for this GitLab user.': '此 GitLab 用户无法获取评审人 ID',
|
||||
'Create a {{value0}} to start checks and review.': '创建 {{value0}} 以开始检查和评审',
|
||||
'Conflicted files are reviewed separately': '冲突文件单独评审',
|
||||
'This review snapshot no longer has any live unresolved conflicts.':
|
||||
'此评审快照不再有任何未解决的实时冲突。',
|
||||
'Skill command copied and inserted below for review.': '技能命令已复制并插入到下方以供评审。',
|
||||
'此评审快照不再有任何未解决的实时冲突',
|
||||
'Skill command copied and inserted below for review.': '技能命令已复制并插入到下方以供评审',
|
||||
'Release readiness review': '发布就绪评审',
|
||||
'Recurring review': '定期评审',
|
||||
Completed: '已完成',
|
||||
@@ -91,9 +91,9 @@ export const ZH_VALUE_OVERRIDES = {
|
||||
'Quick actions': '快捷操作',
|
||||
'action recipes': '操作方案',
|
||||
'Repository action recipes. Global settings are used until this repository customizes them.':
|
||||
'仓库操作方案。在此仓库自定义之前,将使用全局设置。',
|
||||
'仓库操作方案。在此仓库自定义之前,将使用全局设置',
|
||||
'Adds action recipes for Source Control commit, pull request, branch-name, and fix actions.':
|
||||
'为源代码管理的提交、拉取请求、分支命名和修复操作添加操作方案。',
|
||||
'为源代码管理的提交、拉取请求、分支命名和修复操作添加操作方案',
|
||||
// Picker siblings are translated; English pins here would regenerate half-Chinese UI.
|
||||
Play: '播放',
|
||||
Flag: '标记',
|
||||
@@ -121,39 +121,39 @@ export const ZH_VALUE_OVERRIDES = {
|
||||
'floating terminal': '浮动终端',
|
||||
'New Issue': '新建议题',
|
||||
'No issues match the selected filters. Clear a filter or try different criteria.':
|
||||
'没有议题符合所选筛选条件。请清除筛选条件或尝试其他条件。',
|
||||
'没有议题符合所选筛选条件。请清除筛选条件或尝试其他条件',
|
||||
'No issues in this workspace scope. Try searching or adjusting teams.':
|
||||
'此工作区范围内没有议题。请尝试搜索或调整团队。',
|
||||
'此工作区范围内没有议题。请尝试搜索或调整团队',
|
||||
'Close as duplicate': '作为重复议题关闭',
|
||||
'Reply in this review thread': '在此评审线程中回复',
|
||||
'Failed to add review comment.': '添加评审评论失败。',
|
||||
'Failed to add review comment.': '添加评审评论失败',
|
||||
'Localhost Worktree Labels': 'localhost 工作树标签',
|
||||
'Open workspace ports as worktree-specific Orca localhost URLs so browser tabs are easier to tell apart.':
|
||||
'将工作区端口作为工作树专属的 Orca localhost URL 打开,便于区分浏览器标签页。',
|
||||
'将工作区端口作为工作树专属的 Orca localhost URL 打开,便于区分浏览器标签页',
|
||||
'Permanently deletes the remote Git worktrees and their branches. Cannot be undone.':
|
||||
'永久删除远程 Git 工作树及其分支。此操作不可撤销。',
|
||||
'永久删除远程 Git 工作树及其分支。此操作不可撤销',
|
||||
'Clears them from Orca only. Remote files, worktrees, and branches are left untouched.':
|
||||
'仅从 Orca 中清除。远程文件、工作树和分支均不受影响。',
|
||||
'仅从 Orca 中清除。远程文件、工作树和分支均不受影响',
|
||||
'GitHub reports this pull request is blocked': 'GitHub 报告此拉取请求已被阻塞',
|
||||
'GitHub requires review approval before this pull request can merge':
|
||||
'GitHub 需要评审批准才能合并此拉取请求',
|
||||
'Used by repositories that inherit global hosted-review defaults.':
|
||||
'由继承全局托管评审默认值的仓库使用。',
|
||||
'由继承全局托管评审默认值的仓库使用',
|
||||
'Use review template when available': '可用时使用评审模板',
|
||||
'Create hosted reviews as drafts unless changed in the composer.':
|
||||
'除非在编辑器中更改,否则将托管评审创建为草稿。',
|
||||
'Start an agent from failed hosted-review checks.': '从失败的托管评审检查中启动智能体。',
|
||||
'除非在编辑器中更改,否则将托管评审创建为草稿',
|
||||
'Start an agent from failed hosted-review checks.': '从失败的托管评审检查中启动智能体',
|
||||
'Checks require a Git branch and hosted review context': '检查需要 Git 分支和托管评审上下文',
|
||||
'Resolve Review Conflicts With AI': '使用 AI 解决评审冲突',
|
||||
'Hosted review operation in progress…': '托管评审操作进行中…',
|
||||
'Hosted review creation defaults': '托管评审创建默认值',
|
||||
'Open hosted review after creation': '创建后打开托管评审',
|
||||
'Hosted review creation defaults used when the hosted review editor opens.':
|
||||
'打开托管评审编辑器时使用的托管评审创建默认值。',
|
||||
'打开托管评审编辑器时使用的托管评审创建默认值',
|
||||
'Run hosted review detail generation once when the editor opens.':
|
||||
'编辑器打开时运行一次托管评审详情生成。',
|
||||
'编辑器打开时运行一次托管评审详情生成',
|
||||
'Show local Markdown review notes controls in rich editor mode.':
|
||||
'在富文本编辑器模式下显示本地 Markdown 评审笔记控件。',
|
||||
'在富文本编辑器模式下显示本地 Markdown 评审笔记控件',
|
||||
'Orca IDE': 'Orca IDE',
|
||||
'Orca first': 'Orca 优先',
|
||||
'Orca Mobile': 'Orca Mobile',
|
||||
@@ -169,57 +169,57 @@ export const ZH_VALUE_OVERRIDES = {
|
||||
'Active Codex Account': '活跃 Codex 账户',
|
||||
'Claude Accounts': 'Claude 账户',
|
||||
'Remove Claude Account?': '删除 Claude 账户?',
|
||||
'Claude account updated.': 'Claude 账户已更新。',
|
||||
'Claude account update failed.': 'Claude 账户更新失败。',
|
||||
'Could not load Claude accounts.': '无法加载 Claude 账户。',
|
||||
'Claude sign-in failed.': 'Claude 登录失败。',
|
||||
'Claude account added.': '已添加 Claude 账户。',
|
||||
'Claude account updated.': 'Claude 账户已更新',
|
||||
'Claude account update failed.': 'Claude 账户更新失败',
|
||||
'Could not load Claude accounts.': '无法加载 Claude 账户',
|
||||
'Claude sign-in failed.': 'Claude 登录失败',
|
||||
'Claude account added.': '已添加 Claude 账户',
|
||||
'Claude Code session started': 'Claude Code 会话已开始',
|
||||
'Codex session started': 'Codex 会话已开始',
|
||||
'Enable Codex': '启用 Codex',
|
||||
'Most recent local Codex sessions in this scope.': '此范围内最近的本地 Codex 会话。',
|
||||
'Most recent local Codex sessions in this scope.': '此范围内最近的本地 Codex 会话',
|
||||
'Hosted-review creation defaults': '托管评审创建默认值',
|
||||
'Defaults used when the hosted-review composer opens.': '打开托管评审编辑器时使用的默认值。',
|
||||
'Defaults used when the hosted-review composer opens.': '打开托管评审编辑器时使用的默认值',
|
||||
'Run hosted-review detail generation once when the composer opens.':
|
||||
'编辑器打开时运行一次托管评审详情生成。',
|
||||
'编辑器打开时运行一次托管评审详情生成',
|
||||
'Show local markdown review note controls in rich editor mode.':
|
||||
'在富文本编辑器模式下显示本地 Markdown 评审笔记控件。',
|
||||
'在富文本编辑器模式下显示本地 Markdown 评审笔记控件',
|
||||
'Start an agent for local or hosted-review merge conflicts.':
|
||||
'启动用于解决本地或托管评审合并冲突的智能体。',
|
||||
'启动用于解决本地或托管评审合并冲突的智能体',
|
||||
'changed since you last approved. Re-review before it runs':
|
||||
'自您上次批准以来已发生变化。运行前请重新评审',
|
||||
'Run the weekly dependency audit and summarize risky changes.':
|
||||
'每周运行依赖项审计并总结有风险的更改。',
|
||||
'每周运行依赖项审计并总结有风险的更改',
|
||||
'This turns on a process-wide Electron networking switch after restart. Use it for corporate VPNs or proxies that reject HTTP/2 update downloads.':
|
||||
'这将在重启后启用进程级 Electron 网络开关。适用于拒绝 HTTP/2 更新下载的企业 VPN 或代理。',
|
||||
'这将在重启后启用进程级 Electron 网络开关。适用于拒绝 HTTP/2 更新下载的企业 VPN 或代理',
|
||||
'Use only when a corporate VPN or proxy breaks update downloads with HTTP/2 protocol errors. It affects all Electron networking after restart.':
|
||||
'仅当企业 VPN 或代理因 HTTP/2 协议错误而中断更新下载时使用。重启后会影响所有 Electron 网络。',
|
||||
'仅当企业 VPN 或代理因 HTTP/2 协议错误而中断更新下载时使用。重启后会影响所有 Electron 网络',
|
||||
'Use HTTP/1.1 for Electron networking when HTTP/2 fails behind a proxy.':
|
||||
'当 HTTP/2 在代理后面失败时,使用 HTTP/1.1 进行 Electron 网络。',
|
||||
'当 HTTP/2 在代理后面失败时,使用 HTTP/1.1 进行 Electron 网络',
|
||||
'Proxy Bypass Rules': '代理绕过规则',
|
||||
'Hosts that should bypass the configured HTTP proxy.': '应绕过配置的 HTTP 代理的主机。',
|
||||
'Hosts that should bypass the configured HTTP proxy.': '应绕过配置的 HTTP 代理的主机',
|
||||
'Leave empty to use system proxy settings and inherited proxy environment variables.':
|
||||
'留空以使用系统代理设置和继承的代理环境变量。',
|
||||
'留空以使用系统代理设置和继承的代理环境变量',
|
||||
'Proxy Command': '代理命令',
|
||||
"Give agents direct access to Orca's browser so they can test pages, capture screenshots, and act on what they see.":
|
||||
'让智能体直接访问 Orca 的浏览器,以便测试页面、捕获屏幕截图并根据所见内容执行操作。',
|
||||
'让智能体直接访问 Orca 的浏览器,以便测试页面、捕获屏幕截图并根据所见内容执行操作',
|
||||
'X finishes, send it the review task.”': 'X 完成后,把评审任务发给它。”',
|
||||
'Branch naming, base refs, and Git AI Author.': '分支命名、基础引用和 Git AI Author。',
|
||||
'Branch naming, base refs, and Git AI Author.': '分支命名、基础引用和 Git AI Author',
|
||||
'You have unsaved Git AI Author changes. Leaving will discard them.':
|
||||
'您有未保存的 Git AI Author 更改。离开将丢弃它们。',
|
||||
'您有未保存的 Git AI Author 更改。离开将丢弃它们',
|
||||
'Discard unsaved Git AI Author changes?': '放弃未保存的 Git AI Author 更改?',
|
||||
'Create a Linear project for the selected team.': '为所选团队创建 Linear 项目。',
|
||||
'Create a Linear project for the selected team.': '为所选团队创建 Linear 项目',
|
||||
'Linear task mode': 'Linear 任务模式',
|
||||
'Failed to switch Linear workspace.': '无法切换 Linear 工作区。',
|
||||
'Linear project is missing workspace context.': 'Linear 项目缺少工作区上下文。',
|
||||
'Failed to switch Linear workspace.': '无法切换 Linear 工作区',
|
||||
'Linear project is missing workspace context.': 'Linear 项目缺少工作区上下文',
|
||||
'No Linear projects found': '未找到 Linear 项目',
|
||||
'Unable to load Linear projects': '无法加载 Linear 项目',
|
||||
'Change Linear state from {{value0}}': '将 Linear 状态从 {{value0}} 更改为',
|
||||
'GitHub & Linear tasks': 'GitHub 和 Linear 任务',
|
||||
'Each connected Linear workspace has one key stored by the active runtime. Full-access keys can cover all teams the key owner can access; restricted keys can be replaced any time.':
|
||||
'每个已连接的 Linear 工作区都有一个由活动运行时存储的密钥。全权限密钥可覆盖密钥所有者可访问的所有团队;受限密钥可随时更换。',
|
||||
'每个已连接的 Linear 工作区都有一个由活动运行时存储的密钥。全权限密钥可覆盖密钥所有者可访问的所有团队;受限密钥可随时更换',
|
||||
'Show Linear in the Tasks source picker and sidebar shortcuts.':
|
||||
'在任务源选择器和侧边栏快捷方式中显示 Linear。',
|
||||
'在任务源选择器和侧边栏快捷方式中显示 Linear',
|
||||
'Local project, Git repo, or folder with many repos': '本地项目、Git 仓库或包含多个仓库的文件夹',
|
||||
'Staged Changes': '已暂存的更改',
|
||||
Changes: '更改',
|
||||
@@ -231,12 +231,12 @@ export const ZH_VALUE_OVERRIDES = {
|
||||
'Split Terminal Down': '向下拆分终端',
|
||||
'Split Terminal Right': '向右拆分终端',
|
||||
'Optional account switching for Claude while preserving shared chat context.':
|
||||
'Claude 的可选账户切换,同时保留共享聊天上下文。',
|
||||
'Claude 的可选账户切换,同时保留共享聊天上下文',
|
||||
'Countdown timer showing time until prompt cache expires (Claude agents).':
|
||||
'显示提示词缓存到期倒计时的计时器(Claude 智能体)。',
|
||||
'显示提示词缓存到期倒计时的计时器(Claude 智能体)',
|
||||
'Claude caches your conversation to reduce costs. When idle too long the cache expires and the next message resends full context at higher cost. This shows a countdown so you know when to resume.':
|
||||
'Claude 会缓存对话以降低成本。空闲过久后缓存会过期,下一条消息将以更高成本重新发送完整上下文。此倒计时可帮助您了解何时继续。',
|
||||
'from Orca. It is still on your disk.': '来自 Orca。它仍保留在您的磁盘上。',
|
||||
'Claude 会缓存对话以降低成本。空闲过久后缓存会过期,下一条消息将以更高成本重新发送完整上下文。此倒计时可帮助您了解何时继续',
|
||||
'from Orca. It is still on your disk.': '来自 Orca。它仍保留在您的磁盘上',
|
||||
'Pull request reopened': '拉取请求已重新打开',
|
||||
'Reopen PR': '重新打开 PR',
|
||||
'Reopened MR !{{value0}}': '已重新打开 MR !{{value0}}',
|
||||
|
||||
@@ -41,7 +41,8 @@ const USER_VISIBLE_FUNCTION_NAMES = new Set([
|
||||
'confirm',
|
||||
'prompt',
|
||||
'showError',
|
||||
'showToast'
|
||||
'showToast',
|
||||
'toast'
|
||||
])
|
||||
const USER_VISIBLE_OBJECT_METHODS = new Set([
|
||||
'error',
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Arm } from './native-chat-4a-arm'
|
||||
import type { Manifest } from './native-chat-4a-fixture'
|
||||
import { profileRunner } from './native-chat-4a-run-context'
|
||||
import * as memory from './native-chat-4a-ops-memory'
|
||||
import { convertedMemoryRows } from './native-chat-4a-memory-profile'
|
||||
import type { ArmResult, Row } from './native-chat-4a-report'
|
||||
import { seededDbFiles } from './native-chat-4a-sqlite-observer'
|
||||
import type { Workload } from './native-chat-4a-workloads'
|
||||
@@ -146,7 +147,7 @@ export async function runArm(input: {
|
||||
source.seedDir,
|
||||
source.manifest,
|
||||
'memory',
|
||||
memory.convertedMemoryRows,
|
||||
convertedMemoryRows,
|
||||
memory.memoryWorkload(workload)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Arm } from './native-chat-4a-arm'
|
||||
import { assertOneRun, parseArmSpec, seededBy } from './native-chat-4a-arm-provenance'
|
||||
import { FAMILIES, runArm, type Family } from './native-chat-4a-comparison-arm'
|
||||
import * as memory from './native-chat-4a-ops-memory'
|
||||
import { seedMemoryProfile } from './native-chat-4a-memory-profile'
|
||||
import { armMarkdown, writeComparison, type ArmResult } from './native-chat-4a-report'
|
||||
import {
|
||||
conversionToolOutputBytes,
|
||||
@@ -104,7 +105,7 @@ async function main(): Promise<void> {
|
||||
? seededBy(seedArm, await seed(workload.history.toolOutputBytes, workload))
|
||||
: null
|
||||
const { memoryProfile, memorySeedFailure } = families.has('memory')
|
||||
? await memory.seedMemoryProfile(
|
||||
? await seedMemoryProfile(
|
||||
out,
|
||||
workload,
|
||||
async (seeded, runDir) =>
|
||||
|
||||
@@ -120,15 +120,15 @@ export function memoryMarkdown(base: ArmResult, next?: ArmResult): string[] {
|
||||
const heaps = workers
|
||||
.map((worker) => {
|
||||
const memory = object(worker.memory)
|
||||
return `${memory.threadId ?? worker.sessionId}: ${memory.heapUsed ?? 'unavailable'}`
|
||||
return `${String(memory.threadId ?? worker.sessionId)}: ${String(memory.heapUsed ?? 'unavailable')}`
|
||||
})
|
||||
.join(', ')
|
||||
const status =
|
||||
inventory.complete === true
|
||||
? JSON.stringify(inventory.roleCounts)
|
||||
: `unexercised: ${sample?.evidence?.workerError ?? JSON.stringify(inventory.errors) ?? 'inventory unavailable'}`
|
||||
: `unexercised: ${String(sample?.evidence?.workerError ?? JSON.stringify(inventory.errors) ?? 'inventory unavailable')}`
|
||||
lines.push(
|
||||
`| ${result.arm} | ${row.variant} | ${status} | ${heaps} | ${inventory.processRssBytes ?? 'unavailable'} |`
|
||||
`| ${result.arm} | ${row.variant} | ${status} | ${heaps} | ${String(inventory.processRssBytes ?? 'unavailable')} |`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,5 +284,3 @@ export async function memoryRows(ctx: RunContext): Promise<void> {
|
||||
delta('before activation post-GC', 'after activation post-GC', 'activation delta post-GC')
|
||||
}
|
||||
}
|
||||
|
||||
export { seedMemoryProfile, convertedMemoryRows } from './native-chat-4a-memory-profile'
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ const layoutFunctions = layoutAst.statements.filter(
|
||||
)
|
||||
assert.equal(layoutFunctions.length, layoutNames.length)
|
||||
const layoutSubset = `import { isTerminalLeafId } from ${JSON.stringify(path.resolve('src/shared/workspace-layout/stable-pane-id.ts'))};
|
||||
import { readSplitRatio } from '@/lib/pane-manager/pane-split-ratio';
|
||||
import { readSplitRatio } from '@/lib/pane-manager/pane-layout/pane-split-ratio';
|
||||
import { SPLIT_RATIO_TOLERANCE } from ${JSON.stringify(path.resolve('src/shared/workspace-layout/terminal-pane-tree.ts'))};
|
||||
${layoutFunctions.map((node) => node.getText(layoutAst)).join('\n')}`
|
||||
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ const EFFECT_BOUNDARIES = new Set([
|
||||
`${WATCHERS}terminal-parked-pty-watcher.ts#startParkedPtyWatcher`,
|
||||
`${RENDERER}components/terminal-pane/parking/capture/parked-terminal-buffer-capture.ts#captureParkedTerminalBuffers`,
|
||||
`${RENDERER}lib/agent-session/resume/sleeping-agent-session-launch.ts#launchSleepingAgentSession`,
|
||||
`${RENDERER}lib/host-mirror-handle-gap-wait.ts#parkUntilHostMirrorHandleLands`,
|
||||
`${RENDERER}lib/agent-session/resume/host-mirror-handle-gap-wait.ts#parkUntilHostMirrorHandleLands`,
|
||||
`${RENDERER}runtime/web-session-tabs-sync/hydration/host-session-mirror-hydration.ts#parkUntilHostSessionMirrorHydrates`
|
||||
])
|
||||
const ENUMERATORS = new Set(['entries', 'values', 'keys', 'assign'])
|
||||
|
||||
@@ -92,9 +92,9 @@ export const PR_FEATURE_AREA_E2E_SOURCE_ROUTES = [
|
||||
['issue-12656-terminal-link-tooltip', 'terminal-pane-padding-edges'],
|
||||
[
|
||||
'src/renderer/src/assets/terminal.css',
|
||||
'src/renderer/src/lib/pane-manager/pane-divider.ts',
|
||||
'src/renderer/src/lib/pane-manager/pane-layout/pane-divider.ts',
|
||||
'src/renderer/src/lib/pane-manager/pane-dom-creation.ts',
|
||||
'src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset-on-mouseleave.ts'
|
||||
'src/renderer/src/lib/pane-manager/terminal-input/terminal-linkifier-hover-reset-on-mouseleave.ts'
|
||||
]
|
||||
),
|
||||
featureRoute(
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
4 src/renderer/src/components/dashboard/build-dashboard-bucket-counts.ts # P5b-2 must clear: optional state fields; memo compare goes stale when absent
|
||||
3 src/renderer/src/components/dashboard/dashboard-card-terminal-input.ts # P5b-2 must clear: optional state fields default to empty
|
||||
1 src/renderer/src/components/dashboard/dashboard-snapshot-workspaces.ts # P5b-2 must clear: optional state field defaults to empty
|
||||
1 src/renderer/src/components/dashboard/dashboard-worktree-launch-options.ts # P5b-2 must clear: optional state field defaults to empty
|
||||
4 src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts # P5b-2 must clear: change detection on optional fields never fires when absent
|
||||
2 src/renderer/src/components/dashboard/useRetainedAgents.ts # reads a passed-in copy
|
||||
1 src/renderer/src/components/editor/markdown/documents/markdown-document-worktree-path-selector.ts # P5b-2 must clear: optional state field
|
||||
@@ -31,14 +30,14 @@
|
||||
1 src/renderer/src/lib/browser/navigation/http-link-localhost-route.ts # P5b-2 must clear: optional state field defaults to empty; extracted from http-link-routing.ts
|
||||
1 src/renderer/src/lib/workspace/ownership/connection-owner-resolution.ts # P5b-2 must clear: optional-chain compare of a previous slice
|
||||
2 src/renderer/src/lib/workspace/ownership/direct-ssh-target-scope.ts # P5b-2 must clear: optional input fields default to empty
|
||||
3 src/renderer/src/lib/duplicate-paired-servers.ts # P5b-2 must clear: optional state fields default to empty
|
||||
3 src/renderer/src/lib/workspace/ownership/duplicate-paired-servers.ts # P5b-2 must clear: optional state fields default to empty
|
||||
1 src/renderer/src/lib/workspace/ownership/folder-workspace-runtime-owner.ts # P5b-2 must clear: loosely typed state param
|
||||
1 src/renderer/src/runtime/hosts/wsl/local-project-runtime-workspace.ts # P5b-2 must clear: optional state field defaults to empty
|
||||
1 src/renderer/src/lib/resolved-worktree-execution-host.ts # P5b-2 must clear: loosely typed state param
|
||||
1 src/renderer/src/lib/workspace/ownership/resolved-worktree-execution-host.ts # P5b-2 must clear: loosely typed state param
|
||||
2 src/renderer/src/lib/workspace/ownership/runtime-session-mirror-owners.ts # P5b-2 must clear: optional state fields default to empty
|
||||
2 src/renderer/src/lib/workspace/ownership/terminal-worktree-route.ts # P5b-2 must clear: the unattributed-twin check reads both catalogs directly
|
||||
3 src/renderer/src/lib/typing-latency/diagnostic-summary.ts # P5b-2 must clear: optional state field
|
||||
2 src/renderer/src/lib/unread-badge-count.ts # reads a passed-in copy
|
||||
3 src/renderer/src/diagnostics/typing-latency/diagnostic-summary.ts # P5b-2 must clear: optional state field
|
||||
2 src/renderer/src/attention/unread/unread-badge-count.ts # reads a passed-in copy
|
||||
1 src/renderer/src/lib/workspace/paths/workspace-directory.ts # P5b-2 must clear: optional state field defaults to empty
|
||||
2 src/renderer/src/lib/workspace/ownership/workspace-terminal-host-authority.ts # P5b-2 must clear: optional state fields
|
||||
1 src/renderer/src/lib/workspace/activation/initial-surface/worktree-initial-terminal-seeding.ts # P5b-2 must clear: treats an undefined field as a narrow adapter
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="113" height="20" role="img" aria-label="downloads: 168m">
|
||||
<title>downloads: 168m</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="113" height="20" role="img" aria-label="downloads: 169m">
|
||||
<title>downloads: 169m</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
@@ -15,7 +15,7 @@
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text x="37" y="15" fill="#010101" fill-opacity=".3">downloads</text>
|
||||
<text x="37" y="14">downloads</text>
|
||||
<text x="93.5" y="15" fill="#010101" fill-opacity=".3">168m</text>
|
||||
<text x="93.5" y="14">168m</text>
|
||||
<text x="93.5" y="15" fill="#010101" fill-opacity=".3">169m</text>
|
||||
<text x="93.5" y="14">169m</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 943 B After Width: | Height: | Size: 943 B |
@@ -233,6 +233,21 @@ orca chat rename --name "Migrate auth"
|
||||
|
||||
Renames a chat tab, the same as renaming it by hand. `--session` takes the `sessionId` that `orca agent launch` prints. Run inside a chat, it defaults to that chat; with `--environment` it is required.
|
||||
|
||||
Agents that run as chats are read and messaged with `chat` commands. `--session` takes the session id from `chat list` or the handle `agent launch` returns.
|
||||
|
||||
```bash
|
||||
orca chat list --json
|
||||
orca chat list --state working --json
|
||||
orca chat read --session <session> --limit 10 --json
|
||||
orca chat send --session <session> --text "Run the tests, then summarize failures" --json
|
||||
orca chat send --session <session> --text "<message>" --wait --timeout-ms 600000 --json
|
||||
orca chat wait --session <session> --timeout-ms 600000 --json
|
||||
```
|
||||
|
||||
`chat read` returns the newest messages, tool calls, errors, approvals and questions; an approval or question waiting for an answer has `pending: true`. `chat send` sends the text as your message. While the agent is working, hosts with a message queue hold it until the turn ends and report `delivery: "queued"`. A chat that restarted since it was read refuses the send. `--text-file -` reads the message from stdin.
|
||||
|
||||
`chat send --wait` and `chat wait` return once the chat's own turn settles, in the words `chat list` uses: `settled: "done"`, or `settled: "blocked"` when an approval or question needs an answer. Background agents the chat started can still show as `working` or `waiting` in `chat list`. They print the entries since the last user message. A wait past `--timeout-ms` returns `settled: "timeout"`, and a message held in a stopped or failed queue returns `settled: "held"`; both exit 1. `chat read --json` returns a `cursor` for the newest row read, and `truncated: true` when older rows it needed were past the 200-row read. `chat list --state` takes `working`, `blocked`, `waiting` or `done`. Paired hosts (`--environment`, `--pairing-code`) can list chats but not read, send or wait yet.
|
||||
|
||||
## Files
|
||||
|
||||
```bash
|
||||
@@ -412,7 +427,7 @@ A selection applies to every agent of that provider launched on the host after i
|
||||
|
||||
## Artifacts
|
||||
|
||||
Publish HTML or Markdown through the signed-in Orca account. Viewing a public link does not require sign-in; create/list/update/delete do. **Publishing is off by default** — a human must enable **Settings → Artifacts → Allow publishing public artifact links** on the device. There is no CLI flag that grants the gate. `list`, `unshare`, and `delete` stay available so you can audit or revoke links after turning publishing off.
|
||||
Publish HTML or Markdown through the signed-in Orca account. Viewing a public link does not require sign-in; create/list/update/delete do. Publishing follows **Settings → Artifacts → Allow publishing public artifact links** on the device: on by default for new profiles, while profiles created before this default keep their previous setting. Only a human can change it; there is no CLI flag that grants the gate. `list`, `unshare`, and `delete` stay available so you can audit or revoke links after turning publishing off.
|
||||
|
||||
```bash
|
||||
orca artifacts share ./report.html --json
|
||||
|
||||
@@ -51,4 +51,4 @@ For long documents, click the **Table of Contents** button (tree icon) in the ed
|
||||
|
||||
## Share as artifact
|
||||
|
||||
From the editor header, **Share as artifact** publishes the open Markdown file as a public view link through your signed-in Orca account. Requires **Settings → Artifacts → Allow publishing public artifact links** (off by default). Republish updates the same link when Orca still has the mapping; manage or delete links from the sidebar **Artifacts** page. Agents and scripts can use `orca artifacts share|update|list|delete` — see [CLI reference → Artifacts](/docs/cli/reference#artifacts).
|
||||
From the editor header, **Share as artifact** publishes the open Markdown file as a public view link through your signed-in Orca account. Requires **Settings → Artifacts → Allow publishing public artifact links** (on by default for new profiles). Republish updates the same link when Orca still has the mapping; manage or delete links from the sidebar **Artifacts** page. Agents and scripts can use `orca artifacts share|update|list|delete` — see [CLI reference → Artifacts](/docs/cli/reference#artifacts).
|
||||
|
||||
@@ -91,7 +91,7 @@ Settings are grouped into panes. Everything here is searchable with `Cmd-,` then
|
||||
## Artifacts
|
||||
|
||||
- **Orca account** — sign in (same account family as Orca Relay) to publish and manage shared files.
|
||||
- **Allow publishing public artifact links** — **off by default**. Device-wide gate: when on, you, agents, and the `orca` CLI on this machine can upload HTML/Markdown and mint public view links. Turning it off does not delete existing links.
|
||||
- **Allow publishing public artifact links** — **on by default for new profiles**; profiles created before this default keep their previous setting. Device-wide gate: when on, you, agents, and the `orca` CLI on this machine can upload HTML/Markdown and mint public view links. Turning it off does not delete existing links.
|
||||
- **Show Artifacts** / sidebar shortcut — open the Artifacts list to search, preview, copy, or delete account-owned links.
|
||||
- **Ask Before Deleting Artifacts** — optional confirmation before breaking a public link.
|
||||
- Share from an open local HTML page or Markdown editor with **Share as artifact**, or via `orca artifacts …`. See [CLI reference → Artifacts](/docs/cli/reference#artifacts).
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ internal enum class MobileWebShellBridgeInstall {
|
||||
/** The prop is false, so nothing is registered and Phase B behaviour is byte-identical. */
|
||||
SKIP,
|
||||
INSTALL,
|
||||
/** The WebView provider is older than `WEB_MESSAGE_LISTENER` (Chromium 88). Terminal. */
|
||||
/** The WebView provider is older than `WEB_MESSAGE_LISTENER` (Chromium 83). Terminal. */
|
||||
UNAVAILABLE
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ internal class MobileWebShellDocument(private val context: Context) {
|
||||
view.destroy()
|
||||
}
|
||||
|
||||
// Keep explicit settings for the Chromium 83 floor, even where newer WebViews ignore deprecated options.
|
||||
// Keep explicit settings even where newer WebViews ignore deprecated options; the floor is Chromium 101.
|
||||
// B4 requires device proofs: no network, persistent state, file/content access, cleartext or navigation escape.
|
||||
@Suppress("DEPRECATION")
|
||||
private fun createWebView(): WebView {
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ internal val MOBILE_WEB_SHELL_NETWORK_API_BLOCKER = """
|
||||
})();
|
||||
""".trimIndent()
|
||||
|
||||
/** Query document-start-script capability (Chromium 83 floor), not version strings. */
|
||||
/** Query capability, not version strings; stable WebView has it from Chromium 101. */
|
||||
internal fun installMobileWebShellNetworkApiBlocker(
|
||||
webView: WebView,
|
||||
allowedOrigin: String
|
||||
|
||||
+13
@@ -8,6 +8,8 @@ import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.webkit.WebViewCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import expo.modules.kotlin.exception.CodedException
|
||||
import expo.modules.kotlin.exception.Exceptions
|
||||
import expo.modules.kotlin.functions.Queues
|
||||
@@ -68,6 +70,17 @@ class OrcaMobileWebShellModule : Module() {
|
||||
Uri.fromFile(directory).toString()
|
||||
}
|
||||
|
||||
// Lets isolation-unavailable name the WebView to update.
|
||||
Function("currentWebViewProvider") {
|
||||
val context = appContext.reactContext ?: return@Function null
|
||||
val info = WebViewCompat.getCurrentWebViewPackage(context) ?: return@Function null
|
||||
val version = info.versionName ?: return@Function null
|
||||
val name = info.applicationInfo?.loadLabel(context.packageManager)?.toString() ?: info.packageName
|
||||
val hasShellFeatures = WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT) &&
|
||||
WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)
|
||||
mapOf("name" to name, "version" to version, "hasShellFeatures" to hasShellFeatures)
|
||||
}
|
||||
|
||||
AsyncFunction("prepareSpareDocument") { directory: String, sessionId: String ->
|
||||
val activity = appContext.currentActivity ?: return@AsyncFunction false
|
||||
MobileWebShellSparePool.prepare(activity, directory, sessionId)
|
||||
|
||||
@@ -18,7 +18,7 @@ export type OrcaMobileWebShellViewProps = ViewProps & {
|
||||
generationDirectory: string
|
||||
/** `[A-Za-z0-9_-]{1,128}`. Scopes the private origin, so every mount must mint a fresh one. */
|
||||
sessionId: string
|
||||
// Bridge is off by default with nothing registered. Opt-in on Android below WEB_MESSAGE_LISTENER/Chromium 88 fails isolation-unavailable instead of loading unfenced.
|
||||
// Bridge is off by default with nothing registered. Opt-in on Android below WEB_MESSAGE_LISTENER/Chromium 83 fails isolation-unavailable instead of loading unfenced.
|
||||
bridgeEnabled?: boolean
|
||||
onLoadState?: (event: NativeSyntheticEvent<MobileWebShellLoadStatePayload>) => void
|
||||
// Raw JSON from window.orcaBridge; native silently rejects other origins/frames/WebViews and envelopes above 640 KiB (MobileWebShellBridge.maxMessageByteCount).
|
||||
@@ -82,6 +82,23 @@ export function warmUpWebShell(): void {
|
||||
}
|
||||
}
|
||||
|
||||
export type WebViewProvider = { name: string; version: string; hasShellFeatures: boolean }
|
||||
|
||||
// Android only: the WebView provider and whether it has the shell's fences. Null when unknown.
|
||||
export function currentWebViewProvider(): WebViewProvider | null {
|
||||
if (Platform.OS !== 'android') {
|
||||
return null
|
||||
}
|
||||
const shell = requireOptionalNativeModule<{
|
||||
currentWebViewProvider?: () => WebViewProvider | null
|
||||
}>('OrcaMobileWebShell')
|
||||
try {
|
||||
return shell?.currentWebViewProvider?.() ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
MOBILE_WEB_SHELL_FAILURE_REASONS,
|
||||
parseMobileWebShellLoadState,
|
||||
|
||||
@@ -21045,7 +21045,7 @@
|
||||
"id": "codex-reset-credit-consumed",
|
||||
"operation": "accounts.codex-reset-credit",
|
||||
"family": "components.codex-reset-credit",
|
||||
"sites": ["mobile/src/components/codex-reset-credit.ts"],
|
||||
"sites": ["mobile/src/accounts/usage/codex-reset-credit.ts"],
|
||||
"schedules": [],
|
||||
"deviceStore": {},
|
||||
"steps": [
|
||||
|
||||
@@ -16,22 +16,23 @@ import { useMobileTranslation } from '../i18n/use-mobile-translation'
|
||||
import { styles } from './mobile-accounts-screen-styles'
|
||||
import { useNow } from '../hooks/use-now'
|
||||
import { ClaudeIcon, OpenAIIcon } from '../components/AgentIcons'
|
||||
import './usage/UsageBar'
|
||||
import {
|
||||
type ProviderKey,
|
||||
getActiveProviderRateLimits,
|
||||
getInactiveProviderUsage,
|
||||
getOtherUsageProviders,
|
||||
hasActiveProviderUsage
|
||||
} from '../components/AccountUsage'
|
||||
} from './usage/usage-selectors'
|
||||
import { MobileAgentIcon } from '../components/MobileAgentIcon'
|
||||
import { getProviderDisplayName } from '../../../src/shared/usage-provider-windows'
|
||||
import { ProviderUsageWindows } from './ProviderUsageWindows'
|
||||
import {
|
||||
getActiveCodexAccountIdForRateLimitTarget,
|
||||
getCodexResetCreditSummary
|
||||
} from '../components/codex-reset-credit'
|
||||
import { CodexResetCreditAction } from '../components/CodexResetCreditAction'
|
||||
import { useCodexResetCreditAction } from '../components/use-codex-reset-credit-action'
|
||||
} from './usage/codex-reset-credit'
|
||||
import { CodexResetCreditAction } from './usage/CodexResetCreditAction'
|
||||
import { useCodexResetCreditAction } from './usage/use-codex-reset-credit-action'
|
||||
import { ConfirmModal } from '../components/ConfirmModal'
|
||||
import { NavBar } from '../components/nav-bar/NavBar'
|
||||
import { NavBarButton } from '../components/nav-bar/NavBarButton'
|
||||
|
||||
@@ -4,12 +4,9 @@ import {
|
||||
getUsageWindowSections,
|
||||
type UsageWindowSection
|
||||
} from '../../../src/shared/usage-provider-windows'
|
||||
import {
|
||||
getUsageBarState,
|
||||
getWindowResetLabel,
|
||||
UsageBar,
|
||||
type ProviderRateLimits
|
||||
} from '../components/AccountUsage'
|
||||
import { UsageBar } from './usage/UsageBar'
|
||||
import { getUsageBarState, getWindowResetLabel } from './usage/usage-selectors'
|
||||
import type { ProviderRateLimits } from './accounts-snapshot'
|
||||
import type { Translate } from '../i18n/mobile-i18n'
|
||||
import { useMobileTranslation } from '../i18n/use-mobile-translation'
|
||||
import { spacing } from '../theme/mobile-theme'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { queryOptions, skipToken } from '@tanstack/react-query'
|
||||
import { AccountsSnapshotSchema, decodeAccountsSnapshot } from '../components/accounts-snapshot'
|
||||
import type { AccountsSnapshot } from '../components/AccountUsage'
|
||||
import { AccountsSnapshotSchema, decodeAccountsSnapshot } from './accounts-snapshot'
|
||||
import type { AccountsSnapshot } from './accounts-snapshot'
|
||||
import { hostQueryKey, type HostQueryKey } from '../query/host-query-key'
|
||||
import type { PersistedHostFamily } from '../query/host-query-persistence'
|
||||
import type { RpcClient } from '../transport/rpc/rpc-client-contract'
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import { ActivityIndicator, Pressable, StyleSheet, Text } from 'react-native'
|
||||
import { RotateCcw } from 'lucide-react-native'
|
||||
import { useMobileTranslation } from '../i18n/use-mobile-translation'
|
||||
import { colors, feedback, radii, spacing, typography } from '../theme/mobile-theme'
|
||||
import { themedStyleSheet } from '../theme/themed-styles'
|
||||
import { useMobileColorScheme } from '../theme/use-mobile-color-scheme'
|
||||
import { useMobileTranslation } from '../../i18n/use-mobile-translation'
|
||||
import { colors, feedback, radii, spacing, typography } from '../../theme/mobile-theme'
|
||||
import { themedStyleSheet } from '../../theme/themed-styles'
|
||||
import { useMobileColorScheme } from '../../theme/use-mobile-color-scheme'
|
||||
import type { CodexResetCreditSummary } from './codex-reset-credit'
|
||||
import { ListRow } from './list/ListGroup'
|
||||
import { ListRow } from '../../components/list/ListGroup'
|
||||
|
||||
export function CodexResetCreditAction({
|
||||
summary,
|
||||
@@ -1,29 +1,9 @@
|
||||
import { View, Text, ActivityIndicator, type StyleProp, type ViewStyle } from 'react-native'
|
||||
import { colors, spacing, typography } from '../theme/mobile-theme'
|
||||
import { themedStyleSheet } from '../theme/themed-styles'
|
||||
import { useMobileColorScheme } from '../theme/use-mobile-color-scheme'
|
||||
import { colors, spacing, typography } from '../../theme/mobile-theme'
|
||||
import { themedStyleSheet } from '../../theme/themed-styles'
|
||||
import { useMobileColorScheme } from '../../theme/use-mobile-color-scheme'
|
||||
|
||||
// Re-export pure account-usage-state selectors/types to keep imports stable and tests free of RN.
|
||||
export type {
|
||||
RateLimitWindow,
|
||||
ProviderRateLimits,
|
||||
InactiveAccountUsage,
|
||||
ClaudeAccountSummary,
|
||||
CodexAccountSummary,
|
||||
AccountsSnapshot,
|
||||
ProviderKey,
|
||||
UsageBarState
|
||||
} from './account-usage-state'
|
||||
export {
|
||||
decodeAccountsSnapshot,
|
||||
getActiveProviderRateLimits,
|
||||
getInactiveProviderUsage,
|
||||
getOtherUsageProviders,
|
||||
getUsageBarState,
|
||||
getWindowResetLabel,
|
||||
hasActiveProviderUsage,
|
||||
hasRenderableUsage
|
||||
} from './account-usage-state'
|
||||
import './usage-selectors'
|
||||
|
||||
/** A meter's fill: the rounded, clamped percent and its consumption colour, shared by every usage meter. */
|
||||
export function usageBarFill(
|
||||
+4
-7
@@ -1,10 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { CODEX_RESET_CREDIT_RUNTIME_CAPABILITY } from '../../../src/shared/account-runtime-capabilities'
|
||||
import type { RpcClient } from '../transport/rpc/rpc-client-contract'
|
||||
import { startRuntimeCapabilityProbe } from '../transport/connection/runtime-capability-probe'
|
||||
|
||||
// Use shared capability constant so host/mobile versions cannot silently drift.
|
||||
export const MOBILE_CODEX_RESET_CREDIT_CAPABILITY = CODEX_RESET_CREDIT_RUNTIME_CAPABILITY
|
||||
import { CODEX_RESET_CREDIT_RUNTIME_CAPABILITY } from '../../../../src/shared/account-runtime-capabilities'
|
||||
import type { RpcClient } from '../../transport/rpc/rpc-client-contract'
|
||||
import { startRuntimeCapabilityProbe } from '../../transport/connection/runtime-capability-probe'
|
||||
|
||||
export function useCodexResetCreditCapability(
|
||||
client: RpcClient | null,
|
||||
@@ -18,7 +15,7 @@ export function useCodexResetCreditCapability(
|
||||
return
|
||||
}
|
||||
return startRuntimeCapabilityProbe(client, (capabilities) => {
|
||||
setSupported(capabilities.includes(MOBILE_CODEX_RESET_CREDIT_CAPABILITY))
|
||||
setSupported(capabilities.includes(CODEX_RESET_CREDIT_RUNTIME_CAPABILITY))
|
||||
})
|
||||
}, [client, connected])
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc/rpc-operation'
|
||||
import { rpcResultVariant } from '../transport/rpc/rpc-operation-result-reader'
|
||||
import { bindDeferredRpcOperation, defineRpcOperation } from '../../transport/rpc/rpc-operation'
|
||||
import { rpcResultVariant } from '../../transport/rpc/rpc-operation-result-reader'
|
||||
import { codexResetCreditReplySchema } from './codex-reset-credit-reply-schema'
|
||||
|
||||
// Use require-result-or-throw-message so confirm shows host sentence. Keep payload opaque: call-site decodeResetResult alone checks attempt scope/snapshot.
|
||||
+4
-4
@@ -1,20 +1,20 @@
|
||||
import {
|
||||
buildCodexResetCreditExpectedScope,
|
||||
type CodexResetCreditExpectedScope
|
||||
} from '../../../src/shared/codex-reset-credit-scope'
|
||||
} from '../../../../src/shared/codex-reset-credit-scope'
|
||||
import {
|
||||
codexResetCreditConsume,
|
||||
type MobileCodexResetCreditRpcSender,
|
||||
type MobileCodexResetCreditSendScope
|
||||
} from './codex-reset-credit-consume-operations'
|
||||
import { CodexResetCreditExpectedScopeSchema } from './codex-reset-credit-reply-schema'
|
||||
import { randomV4Uuid } from '../platform/random-v4-uuid'
|
||||
import { translate } from '../i18n/mobile-i18n'
|
||||
import { randomV4Uuid } from '../../platform/random-v4-uuid'
|
||||
import { translate } from '../../i18n/mobile-i18n'
|
||||
import {
|
||||
decodeAccountsSnapshot,
|
||||
type AccountsSnapshot,
|
||||
type ProviderRateLimits
|
||||
} from './accounts-snapshot'
|
||||
} from '../accounts-snapshot'
|
||||
|
||||
export type CodexResetCreditOutcome = 'reset' | 'nothingToReset' | 'noCredit' | 'alreadyRedeemed'
|
||||
|
||||
+3
-19
@@ -1,29 +1,13 @@
|
||||
// Keep shapes aligned with shared types/rate-limit-types without pulling Electron-coupled desktop graph into mobile.
|
||||
// Pure RN-free selectors stay directly testable; AccountUsage re-exports them with UsageBar.
|
||||
import { formatResetCountdown } from '../../../src/shared/rate-limit-reset-format'
|
||||
import { hasUsageData } from '../../../src/shared/usage-provider-windows'
|
||||
import { formatResetCountdown } from '../../../../src/shared/rate-limit-reset-format'
|
||||
import { hasUsageData } from '../../../../src/shared/usage-provider-windows'
|
||||
import {
|
||||
ProviderRateLimitsSchema,
|
||||
type AccountsSnapshot,
|
||||
type InactiveAccountUsage,
|
||||
type ProviderRateLimits,
|
||||
type RateLimitWindow
|
||||
} from './accounts-snapshot'
|
||||
|
||||
export {
|
||||
AccountsSnapshotSchema,
|
||||
decodeAccountsSnapshot,
|
||||
ProviderRateLimitsSchema,
|
||||
RateLimitRuntimeTargetSchema,
|
||||
type AccountProviderRateLimits,
|
||||
type AccountsSnapshot,
|
||||
type ClaudeAccountSummary,
|
||||
type CodexAccountSummary,
|
||||
type InactiveAccountUsage,
|
||||
type ProviderRateLimits,
|
||||
type RateLimitRuntimeTarget,
|
||||
type RateLimitWindow
|
||||
} from './accounts-snapshot'
|
||||
} from '../accounts-snapshot'
|
||||
|
||||
export type ProviderKey = 'claude' | 'codex'
|
||||
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import type { CodexResetCreditExpectedScope } from '../../../src/shared/codex-reset-credit-scope'
|
||||
import type { RpcClient } from '../transport/rpc/rpc-client-contract'
|
||||
import { translate } from '../i18n/mobile-i18n'
|
||||
import type { Translate } from '../i18n/mobile-i18n'
|
||||
import { useMobileTranslation } from '../i18n/use-mobile-translation'
|
||||
import type { AccountsSnapshot } from './account-usage-state'
|
||||
import type { CodexResetCreditExpectedScope } from '../../../../src/shared/codex-reset-credit-scope'
|
||||
import type { RpcClient } from '../../transport/rpc/rpc-client-contract'
|
||||
import { translate } from '../../i18n/mobile-i18n'
|
||||
import type { Translate } from '../../i18n/mobile-i18n'
|
||||
import { useMobileTranslation } from '../../i18n/use-mobile-translation'
|
||||
import type { AccountsSnapshot } from '../accounts-snapshot'
|
||||
import {
|
||||
getCodexResetCreditOutcomeCopy,
|
||||
getCodexResetCreditScope,
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { hashKey, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { AccountsSnapshotError, decodeAccountsSnapshot } from '../components/accounts-snapshot'
|
||||
import type { AccountsSnapshot } from '../components/AccountUsage'
|
||||
import { AccountsSnapshotError, decodeAccountsSnapshot } from './accounts-snapshot'
|
||||
import type { AccountsSnapshot } from './accounts-snapshot'
|
||||
import { useMobileTranslation } from '../i18n/use-mobile-translation'
|
||||
import { holdsRestoredHostQueryData } from '../query/host-query-persistence'
|
||||
import type { ConnectionState } from '../transport/connection/connection-state'
|
||||
|
||||
@@ -21,14 +21,7 @@ function oneLine(text: string | null | undefined): string {
|
||||
return text?.replace(/\s+/g, ' ').trim() ?? ''
|
||||
}
|
||||
|
||||
// A worktree agent row or a session tab's AgentStatusEntry, which leaves these optional.
|
||||
type AgentPreviewSource = Pick<RuntimeWorktreeAgentRow, 'prompt'> & {
|
||||
lastAssistantMessage?: string | null
|
||||
toolName?: string | null
|
||||
toolInput?: string | null
|
||||
}
|
||||
|
||||
function lastMessage(agent: AgentPreviewSource): string {
|
||||
function lastMessage(agent: RuntimeWorktreeAgentRow): string {
|
||||
const message = agent.lastAssistantMessage
|
||||
return message ? oneLine(withoutNativeChatVisualDirectiveLines(message)) : ''
|
||||
}
|
||||
@@ -38,7 +31,7 @@ function lastMessage(agent: AgentPreviewSource): string {
|
||||
* (`group` null) shows no tool, since the tool call is long over.
|
||||
*/
|
||||
export function agentListPreview(
|
||||
agent: AgentPreviewSource,
|
||||
agent: RuntimeWorktreeAgentRow,
|
||||
group: AgentStatusGroup | null,
|
||||
title: string
|
||||
): string {
|
||||
|
||||
@@ -15,6 +15,8 @@ export type ActionSheetAction = {
|
||||
destructive?: boolean
|
||||
disabled?: boolean
|
||||
hint?: string
|
||||
/** Short trailing state, e.g. "↑2" or "Up to date". */
|
||||
status?: string
|
||||
/** Marks the choice that is current, as a trailing check. */
|
||||
checked?: boolean
|
||||
loading?: boolean
|
||||
@@ -77,6 +79,7 @@ export function ActionSheetContent({ title, message, children, actions, onClose
|
||||
key={action.id ?? action.label}
|
||||
title={action.label}
|
||||
subtitle={action.hint}
|
||||
value={action.status}
|
||||
leading={
|
||||
action.renderIcon?.() ?? (
|
||||
<Icon
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { Platform, Pressable, Text, View, type ViewStyle } from 'react-native'
|
||||
import { MessageSquare } from 'lucide-react-native'
|
||||
import type { Translate } from '../i18n/mobile-i18n'
|
||||
import { useMobileTranslation } from '../i18n/use-mobile-translation'
|
||||
@@ -7,7 +7,7 @@ import type { DiffComment } from '../../../src/shared/diff-comment-types'
|
||||
import type { MobileDiffLine } from '../source-control/review/mobile-diff-lines'
|
||||
import type { MobileHighlightedDiffLine } from '../files/session-documents/mobile-file-syntax'
|
||||
import { mobileDiffLineNumber, mobileDiffLinePrefix } from '../source-control/mobile-diff-format'
|
||||
import { colors, spacing, typography } from '../theme/mobile-theme'
|
||||
import { colors, typography } from '../theme/mobile-theme'
|
||||
import { themedStyleSheet } from '../theme/themed-styles'
|
||||
import { useMobileColorScheme } from '../theme/use-mobile-color-scheme'
|
||||
import { MobileSyntaxSegments } from './MobileSyntaxSegments'
|
||||
@@ -21,6 +21,8 @@ type Props = {
|
||||
staleCommentIds: ReadonlySet<string>
|
||||
/** Part of the lines picked for a note. */
|
||||
selected?: boolean
|
||||
/** Fits the file's longest line number, so every row's gutter lines up. */
|
||||
numberWidth: number
|
||||
/** Absent where lines take no notes, e.g. a past commit's files. */
|
||||
onAddNote?: (lineNumber: number) => void
|
||||
onEditNote: (comment: DiffComment) => void
|
||||
@@ -52,6 +54,7 @@ export const MobileDiffReviewLine = memo(function MobileDiffReviewLine({
|
||||
comments,
|
||||
staleCommentIds,
|
||||
selected = false,
|
||||
numberWidth,
|
||||
onAddNote,
|
||||
onEditNote
|
||||
}: Props) {
|
||||
@@ -59,23 +62,45 @@ export const MobileDiffReviewLine = memo(function MobileDiffReviewLine({
|
||||
useMobileColorScheme()
|
||||
const lineNumber = mobileDiffLineNumber(line)
|
||||
const canComment = onAddNote !== undefined && canCommentOnLine(line)
|
||||
const ground = [
|
||||
line.kind === 'add' && styles.addedRow,
|
||||
line.kind === 'delete' && styles.deletedRow,
|
||||
selected && styles.selectedRow
|
||||
]
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.row,
|
||||
line.kind === 'add' && styles.addedRow,
|
||||
line.kind === 'delete' && styles.deletedRow,
|
||||
selected && styles.selectedRow
|
||||
]}
|
||||
style={[styles.row, ...ground]}
|
||||
accessible
|
||||
accessibilityLabel={accessibilityLabelForLine(line, t)}
|
||||
{...(canComment && line.newLineNumber !== undefined
|
||||
? { nativeID: `${DIFF_LINE_ID_PREFIX}${line.newLineNumber}` }
|
||||
: {})}
|
||||
>
|
||||
<Text style={styles.prefix}>{mobileDiffLinePrefix(line.kind)}</Text>
|
||||
<Text style={styles.lineNumber}>{lineNumber ? String(lineNumber) : ''}</Text>
|
||||
{/* Pinned while the file scrolls sideways, so it repaints the row's tint over an opaque ground. */}
|
||||
<View style={[styles.pinned, styles.pinnedLeft]}>
|
||||
<View style={[styles.gutter, ...ground]}>
|
||||
<Text
|
||||
style={[
|
||||
styles.lineNumber,
|
||||
{ width: numberWidth },
|
||||
line.kind === 'add' && styles.addedNumber,
|
||||
line.kind === 'delete' && styles.deletedNumber
|
||||
]}
|
||||
>
|
||||
{lineNumber ? String(lineNumber) : ''}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.sign,
|
||||
line.kind === 'add' && styles.addedSign,
|
||||
line.kind === 'delete' && styles.deletedSign
|
||||
]}
|
||||
>
|
||||
{mobileDiffLinePrefix(line.kind)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.code, pressed && canComment && styles.codePressed]}
|
||||
disabled={!canComment}
|
||||
@@ -98,41 +123,83 @@ export const MobileDiffReviewLine = memo(function MobileDiffReviewLine({
|
||||
</Text>
|
||||
</Pressable>
|
||||
{comments.length > 0 ? (
|
||||
<View style={styles.notes}>
|
||||
{comments.map((comment) => (
|
||||
<Pressable
|
||||
key={comment.id}
|
||||
style={({ pressed }) => [styles.noteButton, pressed && styles.noteButtonPressed]}
|
||||
onPress={() => onEditNote(comment)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t(
|
||||
'components.diffReviewLine.editNoteOnLine',
|
||||
'Edit note on line {{number}}',
|
||||
{
|
||||
number: comment.lineNumber
|
||||
}
|
||||
)}
|
||||
>
|
||||
<MessageSquare
|
||||
size={13}
|
||||
color={staleCommentIds.has(comment.id) ? colors.statusAmber : colors.textSecondary}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Pressable>
|
||||
))}
|
||||
<View style={[styles.pinned, styles.pinnedRight]}>
|
||||
<View style={[styles.notes, ...ground]}>
|
||||
{comments.map((comment) => (
|
||||
<Pressable
|
||||
key={comment.id}
|
||||
style={({ pressed }) => [styles.noteButton, pressed && styles.noteButtonPressed]}
|
||||
onPress={() => onEditNote(comment)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t(
|
||||
'components.diffReviewLine.editNoteOnLine',
|
||||
'Edit note on line {{number}}',
|
||||
{
|
||||
number: comment.lineNumber
|
||||
}
|
||||
)}
|
||||
>
|
||||
<MessageSquare
|
||||
size={13}
|
||||
color={
|
||||
staleCommentIds.has(comment.id) ? colors.statusAmber : colors.textSecondary
|
||||
}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
/** Renders one code line's worth of a known string, so the list can size its columns to the code font. */
|
||||
export function MobileDiffReviewCharWidthProbe({
|
||||
onCharWidth
|
||||
}: {
|
||||
onCharWidth: (width: number) => void
|
||||
}) {
|
||||
return (
|
||||
<Text
|
||||
aria-hidden
|
||||
style={[styles.codeText, styles.probe]}
|
||||
onLayout={(event) => onCharWidth(event.nativeEvent.layout.width / PROBE_TEXT.length)}
|
||||
>
|
||||
{PROBE_TEXT}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
const PROBE_TEXT = '0'.repeat(100)
|
||||
const SIGN_WIDTH = 14
|
||||
// Room either side of the line number.
|
||||
const NUMBER_PADDING = 12
|
||||
|
||||
export function mobileDiffReviewGutterWidth(numberWidth: number): number {
|
||||
return numberWidth + SIGN_WIDTH
|
||||
}
|
||||
|
||||
export function mobileDiffReviewNumberWidth(digits: number, charWidth: number): number {
|
||||
return Math.ceil(digits * charWidth + NUMBER_PADDING)
|
||||
}
|
||||
|
||||
// The page lets long lines run and the file scroll sideways under pinned cells (Kelvin's choice over wrapping).
|
||||
const webPinned: ViewStyle =
|
||||
Platform.OS === 'web'
|
||||
? {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: RN Web renders CSS sticky; this branch never reaches native.
|
||||
position: 'sticky' as ViewStyle['position'],
|
||||
zIndex: 1
|
||||
}
|
||||
: {}
|
||||
|
||||
// Use 18px lineHeight alone without padding/minHeight to match desktop diff density (STA-1239).
|
||||
const styles = themedStyleSheet(() => ({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'stretch',
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.borderSubtle
|
||||
alignItems: 'stretch'
|
||||
},
|
||||
addedRow: {
|
||||
backgroundColor: colors.diffAddedBg
|
||||
@@ -144,38 +211,71 @@ const styles = themedStyleSheet(() => ({
|
||||
selectedRow: {
|
||||
backgroundColor: colors.skillTokenSurface
|
||||
},
|
||||
prefix: {
|
||||
width: 18,
|
||||
textAlign: 'center',
|
||||
color: colors.textMuted,
|
||||
pinned: {
|
||||
...webPinned,
|
||||
backgroundColor: colors.bgBase
|
||||
},
|
||||
pinnedLeft: {
|
||||
left: 0
|
||||
},
|
||||
pinnedRight: {
|
||||
right: 0
|
||||
},
|
||||
gutter: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
lineNumber: {
|
||||
paddingRight: 6,
|
||||
textAlign: 'right',
|
||||
color: colors.diffLineNumber,
|
||||
fontFamily: typography.monoFamily,
|
||||
fontSize: typography.metaSize,
|
||||
lineHeight: 18
|
||||
},
|
||||
lineNumber: {
|
||||
width: 44,
|
||||
paddingRight: spacing.xs,
|
||||
textAlign: 'right',
|
||||
color: colors.textMuted,
|
||||
addedNumber: {
|
||||
color: colors.textPrimary,
|
||||
backgroundColor: colors.diffAddedGutter
|
||||
},
|
||||
deletedNumber: {
|
||||
color: colors.textPrimary,
|
||||
backgroundColor: colors.diffDeletedGutter
|
||||
},
|
||||
sign: {
|
||||
width: SIGN_WIDTH,
|
||||
textAlign: 'center',
|
||||
color: colors.diffLineNumber,
|
||||
fontFamily: typography.monoFamily,
|
||||
fontSize: typography.metaSize,
|
||||
lineHeight: 18
|
||||
},
|
||||
addedSign: {
|
||||
color: colors.diffAddedSign
|
||||
},
|
||||
deletedSign: {
|
||||
color: colors.diffDeletedSign
|
||||
},
|
||||
code: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
paddingHorizontal: spacing.sm
|
||||
paddingRight: 10
|
||||
},
|
||||
codePressed: {
|
||||
backgroundColor: colors.bgRaised
|
||||
},
|
||||
codeText: {
|
||||
...(Platform.OS === 'web' ? { whiteSpace: 'pre' as const } : {}),
|
||||
color: colors.textPrimary,
|
||||
fontFamily: typography.monoFamily,
|
||||
fontSize: 12,
|
||||
lineHeight: 18
|
||||
},
|
||||
probe: {
|
||||
position: 'absolute',
|
||||
opacity: 0
|
||||
},
|
||||
notes: {
|
||||
flex: 1,
|
||||
width: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FlatList, Platform, Pressable, Text, View } from 'react-native'
|
||||
import { FlatList, Platform, Pressable, Text, View, type ViewStyle } from 'react-native'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
import { ChevronsUpDown } from 'lucide-react-native'
|
||||
import { UnfoldVertical } from 'lucide-react-native'
|
||||
import type { DiffComment } from '../../../src/shared/diff-comment-types'
|
||||
import { useMobileColorScheme } from '../theme/use-mobile-color-scheme'
|
||||
import { useMobileTranslation } from '../i18n/use-mobile-translation'
|
||||
import { DIFF_LINE_ID_PREFIX, MobileDiffReviewLine } from './MobileDiffReviewLine'
|
||||
import {
|
||||
DIFF_LINE_ID_PREFIX,
|
||||
MobileDiffReviewCharWidthProbe,
|
||||
MobileDiffReviewLine,
|
||||
mobileDiffReviewGutterWidth,
|
||||
mobileDiffReviewNumberWidth
|
||||
} from './MobileDiffReviewLine'
|
||||
import type { ReviewDiffLine } from '../source-control/review/mobile-diff-review-screen-model'
|
||||
import {
|
||||
buildMobileDiffReviewRows,
|
||||
@@ -25,6 +31,18 @@ const RANGE_PICK_LONG_PRESS_MS = 350
|
||||
const MAX_SCROLL_ATTEMPTS = 10
|
||||
const NO_LINE_NOTES: readonly DiffComment[] = []
|
||||
const SCROLL_RETRY_MS = 100
|
||||
// The page's default tab-size, so a tabbed line's width matches what it draws.
|
||||
const TAB_COLUMNS = 8
|
||||
const CODE_END_PADDING = 10
|
||||
const NOTES_WIDTH = 40
|
||||
|
||||
function lineColumns(text: string): number {
|
||||
let columns = 0
|
||||
for (const char of text) {
|
||||
columns = char === '\t' ? (Math.floor(columns / TAB_COLUMNS) + 1) * TAB_COLUMNS : columns + 1
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
export type LineRange = { startLine: number; line: number }
|
||||
|
||||
@@ -77,6 +95,44 @@ export function MobileDiffReviewLines({
|
||||
setOpened({ lines, indexes: next })
|
||||
}
|
||||
|
||||
// Long lines run on one line and the whole file scrolls sideways, so every row is as wide as the longest.
|
||||
const [charWidth, setCharWidth] = useState(0)
|
||||
const [viewportWidth, setViewportWidth] = useState(0)
|
||||
const { numberWidth, codeColumns } = useMemo(() => {
|
||||
let digits = 1
|
||||
let columns = 0
|
||||
for (const line of lines) {
|
||||
digits = Math.max(digits, String(line.newLineNumber ?? line.oldLineNumber ?? '').length)
|
||||
columns = Math.max(columns, lineColumns(line.text))
|
||||
}
|
||||
return { numberWidth: mobileDiffReviewNumberWidth(digits, charWidth), codeColumns: columns }
|
||||
}, [charWidth, lines])
|
||||
const gutterWidth = mobileDiffReviewGutterWidth(numberWidth)
|
||||
const rowFrame: ViewStyle =
|
||||
Platform.OS === 'web' && charWidth > 0
|
||||
? {
|
||||
width: Math.max(
|
||||
viewportWidth,
|
||||
Math.ceil(
|
||||
gutterWidth +
|
||||
codeColumns * charWidth +
|
||||
CODE_END_PADDING +
|
||||
(commentsByLine.size > 0 ? NOTES_WIDTH : 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
: {}
|
||||
// Folds, threads and the footer hold still in the viewport while the code slides under them.
|
||||
const viewportRow: ViewStyle =
|
||||
Platform.OS === 'web' && viewportWidth > 0
|
||||
? {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: RN Web renders CSS sticky; this branch never reaches native.
|
||||
position: 'sticky' as ViewStyle['position'],
|
||||
left: 0,
|
||||
width: viewportWidth
|
||||
}
|
||||
: {}
|
||||
|
||||
const listRef = useRef<FlatList<MobileDiffReviewRow<ReviewDiffLine>>>(null)
|
||||
const scrollFailedRef = useRef(false)
|
||||
const [scrollAttempt, setScrollAttempt] = useState(0)
|
||||
@@ -204,10 +260,16 @@ export function MobileDiffReviewLines({
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={pick}>
|
||||
<View ref={frameRef} style={styles.diffListFrame}>
|
||||
<View
|
||||
ref={frameRef}
|
||||
style={styles.diffListFrame}
|
||||
onLayout={(event) => setViewportWidth(event.nativeEvent.layout.width)}
|
||||
>
|
||||
<MobileDiffReviewCharWidthProbe onCharWidth={setCharWidth} />
|
||||
<FlatList<MobileDiffReviewRow<ReviewDiffLine>>
|
||||
ref={listRef}
|
||||
data={rows}
|
||||
style={styles.diffList}
|
||||
// The list holds still while lines are being picked, so the drag selects instead of scrolling.
|
||||
scrollEnabled={dragRange === null}
|
||||
onScrollToIndexFailed={(info) => {
|
||||
@@ -218,60 +280,77 @@ export function MobileDiffReviewLines({
|
||||
})
|
||||
}}
|
||||
keyExtractor={(row) => row.key}
|
||||
renderItem={({ item: row }) =>
|
||||
row.kind === 'fold' ? (
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.foldRow, pressed && styles.buttonPressed]}
|
||||
onPress={() => openFold(row.startIndex, row.endIndex)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t(
|
||||
'components.diffReviewBody.showUnchangedLines',
|
||||
'Show {{count}} unchanged lines',
|
||||
{ count: row.hiddenCount }
|
||||
)}
|
||||
>
|
||||
<ChevronsUpDown size={13} color={colors.textMuted} strokeWidth={2.2} />
|
||||
<Text style={styles.foldText}>
|
||||
{row.hiddenCount === 1
|
||||
? t(
|
||||
'components.diffReviewBody.unchangedLines_one',
|
||||
'{{count}} unchanged line',
|
||||
{
|
||||
count: row.hiddenCount
|
||||
}
|
||||
)
|
||||
: t(
|
||||
'components.diffReviewBody.unchangedLines_other',
|
||||
'{{count}} unchanged lines',
|
||||
{ count: row.hiddenCount }
|
||||
)}
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : row.kind === 'thread' ? (
|
||||
<View style={styles.threadRow}>
|
||||
<CommentGroupView group={row.thread} actions={threadActions} now={now} />
|
||||
</View>
|
||||
) : (
|
||||
<MobileDiffReviewLine
|
||||
line={row.line}
|
||||
comments={commentsByLine.get(row.line.newLineNumber ?? -1) ?? NO_LINE_NOTES}
|
||||
staleCommentIds={staleCommentIds}
|
||||
selected={
|
||||
highlighted !== null &&
|
||||
row.line.kind !== 'delete' &&
|
||||
row.line.newLineNumber !== undefined &&
|
||||
row.line.newLineNumber >= highlighted.startLine &&
|
||||
row.line.newLineNumber <= highlighted.line
|
||||
}
|
||||
{...(onPickLines ? { onAddNote: pickOneLine } : {})}
|
||||
onEditNote={onEditNote}
|
||||
/>
|
||||
)
|
||||
}
|
||||
contentContainerStyle={styles.diffList}
|
||||
renderItem={({ item: row }) => (
|
||||
<View style={rowFrame}>
|
||||
{row.kind === 'fold' ? (
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.foldRow,
|
||||
viewportRow,
|
||||
pressed && styles.buttonPressed
|
||||
]}
|
||||
onPress={() => openFold(row.startIndex, row.endIndex)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t(
|
||||
'components.diffReviewBody.showUnchangedLines',
|
||||
'Show {{count}} unchanged lines',
|
||||
{ count: row.hiddenCount }
|
||||
)}
|
||||
>
|
||||
<View style={[styles.foldIcon, { width: gutterWidth }]}>
|
||||
<UnfoldVertical size={14} color={colors.diffFoldText} strokeWidth={2} />
|
||||
</View>
|
||||
<View style={styles.foldBody}>
|
||||
<Text style={styles.foldText}>
|
||||
{row.hiddenCount === 1
|
||||
? t(
|
||||
'components.diffReviewBody.unchangedLines_one',
|
||||
'{{count}} unchanged line',
|
||||
{
|
||||
count: row.hiddenCount
|
||||
}
|
||||
)
|
||||
: t(
|
||||
'components.diffReviewBody.unchangedLines_other',
|
||||
'{{count}} unchanged lines',
|
||||
{ count: row.hiddenCount }
|
||||
)}
|
||||
</Text>
|
||||
{row.enclosingLine ? (
|
||||
<Text style={styles.foldContext} numberOfLines={1}>
|
||||
{row.enclosingLine}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</Pressable>
|
||||
) : row.kind === 'thread' ? (
|
||||
<View style={[styles.threadRow, viewportRow]}>
|
||||
<CommentGroupView group={row.thread} actions={threadActions} now={now} />
|
||||
</View>
|
||||
) : (
|
||||
<MobileDiffReviewLine
|
||||
line={row.line}
|
||||
comments={commentsByLine.get(row.line.newLineNumber ?? -1) ?? NO_LINE_NOTES}
|
||||
staleCommentIds={staleCommentIds}
|
||||
numberWidth={numberWidth}
|
||||
selected={
|
||||
highlighted !== null &&
|
||||
row.line.kind !== 'delete' &&
|
||||
row.line.newLineNumber !== undefined &&
|
||||
row.line.newLineNumber >= highlighted.startLine &&
|
||||
row.line.newLineNumber <= highlighted.line
|
||||
}
|
||||
{...(onPickLines ? { onAddNote: pickOneLine } : {})}
|
||||
onEditNote={onEditNote}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
contentContainerStyle={styles.diffListContent}
|
||||
ListFooterComponentStyle={rowFrame}
|
||||
ListFooterComponent={
|
||||
truncated ? (
|
||||
<Text style={styles.truncatedText}>
|
||||
<Text style={[styles.truncatedText, viewportRow]}>
|
||||
{t('components.diffReviewBody.diffTruncated', 'Diff truncated for mobile preview.')}
|
||||
</Text>
|
||||
) : null
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useNow } from '../hooks/use-now'
|
||||
import { commentRelativeTime } from './pr-sidebar/pr-comment-relative-time'
|
||||
import { prStateBadge } from './pr-sidebar/pr-checks-presentation'
|
||||
import { statusColor } from './pr-sidebar/pr-sidebar-status-color'
|
||||
import { PrStateIcon } from './PrStateIcon'
|
||||
import { PrTabEmptyState } from './pr-sidebar/PrTabEmptyState'
|
||||
import { PrChangesCard, PrDescription } from './pr-sidebar/PrTabOverviewCards'
|
||||
import { PrTabBottomAction } from './pr-sidebar/PrTabBottomAction'
|
||||
@@ -231,6 +232,7 @@ function PrTabHeader({ data }: { data: PrSidebarData }) {
|
||||
<View style={styles.header}>
|
||||
<View style={styles.metaRow}>
|
||||
<View style={[styles.badge, { borderColor: badgeColor }]}>
|
||||
<PrStateIcon state={pr.state} size={12} color={badgeColor} strokeWidth={2.2} />
|
||||
<Text style={[styles.badgeText, { color: badgeColor }]}>{badge.label}</Text>
|
||||
</View>
|
||||
<Text style={styles.number}>#{pr.number}</Text>
|
||||
|
||||
@@ -47,5 +47,8 @@ const syntaxTokenStyles: Record<MobileSyntaxTokenKind, TextStyle> = themedStyleS
|
||||
},
|
||||
meta: {
|
||||
color: colors.syntaxMeta
|
||||
},
|
||||
tag: {
|
||||
color: colors.syntaxTag
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createElement } from 'react'
|
||||
import type { LucideProps } from 'lucide-react-native'
|
||||
import { prStateIcon } from './pr-state-token'
|
||||
|
||||
export function PrStateIcon({ state, ...props }: { state: string } & LucideProps) {
|
||||
return createElement(prStateIcon(state), props)
|
||||
}
|
||||
@@ -84,8 +84,6 @@ export function SwipeRowActions({
|
||||
})
|
||||
// Web has no worklet plugin; mapper hooks need this explicit dependency list.
|
||||
const rowStyle = useAnimatedStyle(() => ({ transform: [{ translateX: offset.value }] }), [offset])
|
||||
// Hidden at rest, so the action can't show through a rounded group's antialiased corners.
|
||||
const actionsStyle = useAnimatedStyle(() => ({ opacity: offset.value === 0 ? 0 : 1 }), [offset])
|
||||
// A tap on an open row closes it rather than pressing the row.
|
||||
const guardPress = (onPress: () => void) => () => {
|
||||
// The click a drag release produces belongs to the drag, not to the row.
|
||||
@@ -126,10 +124,8 @@ export function SwipeRowActions({
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
<Animated.View pointerEvents="box-none" style={[styles.actions, actionsStyle]}>
|
||||
{leading ? renderAction(leading, 'leading') : null}
|
||||
{trailing ? renderAction(trailing, 'trailing') : null}
|
||||
</Animated.View>
|
||||
{leading ? renderAction(leading, 'leading') : null}
|
||||
{trailing ? renderAction(trailing, 'trailing') : null}
|
||||
{/* pan-y keeps the page scrolling vertically over the row on the web. */}
|
||||
<GestureDetector gesture={pan} touchAction="pan-y">
|
||||
<Animated.View style={rowStyle}>{children(guardPress)}</Animated.View>
|
||||
@@ -148,7 +144,6 @@ const styles = themedStyleSheet(() => ({
|
||||
justifyContent: 'center',
|
||||
gap: 2
|
||||
},
|
||||
actions: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 },
|
||||
leading: { left: 0 },
|
||||
trailing: { right: 0 },
|
||||
actionPressed: { opacity: feedback.pressedOpacity },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo } from 'react'
|
||||
import { Bell, ChevronDown, ChevronRight, GitBranch, GitPullRequest } from 'lucide-react-native'
|
||||
import { Bell, ChevronDown, ChevronRight, GitBranch } from 'lucide-react-native'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import type { MobileRenderableRepoIcon } from '../host-screen/host-screen-reply-schema'
|
||||
import { triggerMediumImpact } from '../platform/haptics'
|
||||
@@ -20,6 +20,7 @@ import { HostKindIcon } from './HostKindIcon'
|
||||
import { MobileRepoIcon } from './MobileRepoIcon'
|
||||
import { WorktreeAgentList } from './WorktreeAgentList'
|
||||
import { WorktreeMetaGlyphs, prStateColor } from './WorktreeMetaGlyphs'
|
||||
import { PrStateIcon } from './PrStateIcon'
|
||||
import { WorktreeUnsavedName } from './WorktreeUnsavedName'
|
||||
import { WORKTREE_ROW_LAYOUT } from './worktree-list-row-layout'
|
||||
|
||||
@@ -76,6 +77,8 @@ function WorktreeListRowComponent({
|
||||
const showRepoChip = cardDetails.repoInTitle && !shouldHideRepo && !isFolderWorkspace
|
||||
const showRepoInMetaRow = !cardDetails.repoInTitle && !shouldHideRepo
|
||||
const hostId = view.hostContextHostId ?? item.hostId
|
||||
const prState = item.linkedPR?.state ?? ''
|
||||
const prColor = prStateColor(prState)
|
||||
const hostAccessibilityLabel = [view.hostContextLabel, view.hostContextHealthLabel]
|
||||
.filter(Boolean)
|
||||
.join(', ')
|
||||
@@ -153,10 +156,8 @@ function WorktreeListRowComponent({
|
||||
</Text>
|
||||
{cardDetails.pr && item.linkedPR && (
|
||||
<View style={styles.prBadge}>
|
||||
<GitPullRequest size={10} color={prStateColor(item.linkedPR.state)} />
|
||||
<Text style={[styles.prNumber, { color: prStateColor(item.linkedPR.state) }]}>
|
||||
#{item.linkedPR.number}
|
||||
</Text>
|
||||
<PrStateIcon state={prState} size={10} color={prColor} />
|
||||
<Text style={[styles.prNumber, { color: prColor }]}>#{item.linkedPR.number}</Text>
|
||||
</View>
|
||||
)}
|
||||
{isFolderWorkspace && (
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { decodeAccountsSnapshot } from './accounts-snapshot'
|
||||
|
||||
function makeSnapshot(): unknown {
|
||||
return {
|
||||
extensionField: { retained: true },
|
||||
claude: {
|
||||
accounts: [],
|
||||
activeAccountId: null,
|
||||
activeAccountIdsByRuntime: { host: null, wsl: {} }
|
||||
},
|
||||
codex: {
|
||||
accounts: [
|
||||
{
|
||||
id: 'codex-host',
|
||||
email: 'host@example.com',
|
||||
managedHomeRuntime: 'host',
|
||||
wslDistro: null,
|
||||
updatedAt: 100,
|
||||
extensionField: 'account-extra'
|
||||
}
|
||||
],
|
||||
activeAccountId: 'codex-host',
|
||||
activeAccountIdsByRuntime: {
|
||||
host: 'codex-host',
|
||||
wsl: { Ubuntu: 'codex-wsl' }
|
||||
}
|
||||
},
|
||||
rateLimits: {
|
||||
extensionField: 'limits-extra',
|
||||
claude: null,
|
||||
codex: {
|
||||
provider: 'codex',
|
||||
session: {
|
||||
usedPercent: 100,
|
||||
windowMinutes: 300,
|
||||
resetsAt: 200,
|
||||
resetDescription: 'soon'
|
||||
},
|
||||
weekly: null,
|
||||
rateLimitResetCredits: {
|
||||
availableCount: 1,
|
||||
totalEarnedCount: 2,
|
||||
nextExpiresAt: 300,
|
||||
credits: [{ status: 'available', expiresAt: 300, grantedAt: 50 }]
|
||||
},
|
||||
updatedAt: 100,
|
||||
error: null,
|
||||
status: 'ok',
|
||||
extensionField: 'provider-extra'
|
||||
},
|
||||
claudeTarget: { runtime: 'host', wslDistro: null },
|
||||
codexTarget: { runtime: 'host', wslDistro: null },
|
||||
inactiveClaudeAccounts: [],
|
||||
inactiveCodexAccounts: [
|
||||
{
|
||||
accountId: 'codex-inactive',
|
||||
rateLimits: null,
|
||||
updatedAt: 99,
|
||||
isFetching: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setPath(root: unknown, path: string[], value: unknown): void {
|
||||
let current: unknown = root
|
||||
for (const segment of path.slice(0, -1)) {
|
||||
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
||||
throw new Error(`Invalid fixture path: ${path.join('.')}`)
|
||||
}
|
||||
current = (current as Record<string, unknown>)[segment]
|
||||
}
|
||||
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
||||
throw new Error(`Invalid fixture path: ${path.join('.')}`)
|
||||
}
|
||||
const record = current as Record<string, unknown>
|
||||
record[path.at(-1)!] = value
|
||||
}
|
||||
|
||||
describe('decodeAccountsSnapshot', () => {
|
||||
it('validates nested account/rate-limit state and preserves forward-compatible fields', () => {
|
||||
const snapshot = decodeAccountsSnapshot(makeSnapshot())
|
||||
|
||||
expect(snapshot.extensionField).toEqual({ retained: true })
|
||||
expect(snapshot.codex.accounts[0]?.extensionField).toBe('account-extra')
|
||||
expect(snapshot.rateLimits.extensionField).toBe('limits-extra')
|
||||
expect(snapshot.rateLimits.codex?.extensionField).toBe('provider-extra')
|
||||
})
|
||||
|
||||
it('defaults missing runtime targets for older host-only snapshots', () => {
|
||||
const raw = makeSnapshot() as {
|
||||
rateLimits: { claudeTarget?: unknown; codexTarget?: unknown }
|
||||
}
|
||||
delete raw.rateLimits.claudeTarget
|
||||
delete raw.rateLimits.codexTarget
|
||||
|
||||
const snapshot = decodeAccountsSnapshot(raw)
|
||||
|
||||
expect(snapshot.rateLimits.claudeTarget).toEqual({ runtime: 'host', wslDistro: null })
|
||||
expect(snapshot.rateLimits.codexTarget).toEqual({ runtime: 'host', wslDistro: null })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['account arrays', ['codex', 'accounts'], {}],
|
||||
['active account IDs', ['codex', 'activeAccountId'], 42],
|
||||
['runtime selections', ['codex', 'activeAccountIdsByRuntime', 'wsl'], []],
|
||||
['targets', ['rateLimits', 'codexTarget', 'runtime'], 'remote'],
|
||||
['provider identity', ['rateLimits', 'codex', 'provider'], 'claude'],
|
||||
['inactive account arrays', ['rateLimits', 'inactiveCodexAccounts'], {}],
|
||||
['window percentages', ['rateLimits', 'codex', 'session', 'usedPercent'], 101],
|
||||
['credit counts', ['rateLimits', 'codex', 'rateLimitResetCredits', 'availableCount'], -1],
|
||||
[
|
||||
'credit status',
|
||||
['rateLimits', 'codex', 'rateLimitResetCredits', 'credits'],
|
||||
[{ status: '', expiresAt: 300, grantedAt: 50 }]
|
||||
],
|
||||
['credit expiry', ['rateLimits', 'codex', 'rateLimitResetCredits', 'nextExpiresAt'], 'soon']
|
||||
] satisfies Array<[string, string[], unknown]>)('rejects malformed %s', (_name, path, value) => {
|
||||
const snapshot = makeSnapshot()
|
||||
setPath(snapshot, path, value)
|
||||
|
||||
expect(() => decodeAccountsSnapshot(snapshot)).toThrow('Invalid accounts snapshot from host')
|
||||
})
|
||||
|
||||
it('rejects a host target that smuggles a WSL distro', () => {
|
||||
const snapshot = makeSnapshot()
|
||||
setPath(snapshot, ['rateLimits', 'codexTarget', 'wslDistro'], 'Ubuntu')
|
||||
|
||||
expect(() => decodeAccountsSnapshot(snapshot)).toThrow('Invalid accounts snapshot from host')
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StyleSheet } from 'react-native'
|
||||
import { Platform, StyleSheet } from 'react-native'
|
||||
import { colors, radii, spacing, typography } from '../theme/mobile-theme'
|
||||
import { themedStyleSheet } from '../theme/themed-styles'
|
||||
|
||||
@@ -59,13 +59,31 @@ export const mobileDiffReviewLayoutStyles = themedStyleSheet(() => ({
|
||||
foldRow: {
|
||||
minHeight: 32,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'stretch',
|
||||
backgroundColor: colors.diffFoldBg
|
||||
},
|
||||
foldIcon: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: spacing.xs,
|
||||
backgroundColor: colors.bgPanel
|
||||
backgroundColor: colors.diffFoldGutter
|
||||
},
|
||||
foldBody: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
paddingHorizontal: spacing.sm
|
||||
},
|
||||
foldText: {
|
||||
flexShrink: 0,
|
||||
color: colors.diffFoldText,
|
||||
fontSize: typography.metaSize
|
||||
},
|
||||
foldContext: {
|
||||
flexShrink: 1,
|
||||
color: colors.textMuted,
|
||||
fontFamily: typography.monoFamily,
|
||||
fontSize: typography.metaSize
|
||||
},
|
||||
diffListFrame: {
|
||||
@@ -77,8 +95,14 @@ export const mobileDiffReviewLayoutStyles = themedStyleSheet(() => ({
|
||||
backgroundColor: colors.bgBase
|
||||
},
|
||||
diffList: {
|
||||
paddingBottom: 140,
|
||||
backgroundColor: colors.editorSurface
|
||||
flex: 1,
|
||||
backgroundColor: colors.bgBase
|
||||
},
|
||||
// On the page the rows run as wide as the longest line and scroll sideways together. A sideways scroller inside
|
||||
// the vertical list keeps each swipe on one axis, as nested scrollers do in WebKit.
|
||||
diffListContent: {
|
||||
...(Platform.OS === 'web' ? { overflowX: 'auto' as const } : {}),
|
||||
paddingBottom: 140
|
||||
},
|
||||
truncatedText: {
|
||||
color: colors.textMuted,
|
||||
|
||||
@@ -16,6 +16,8 @@ type NavBarButtonProps = {
|
||||
/** The screen or panel this button opens is showing. */
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
/** The icon's colour while not active, e.g. a PR state's. */
|
||||
iconColor?: string
|
||||
}
|
||||
|
||||
// A round top-bar button; the bar floats over content like the composer, so its buttons are round too.
|
||||
@@ -25,7 +27,8 @@ export function NavBarButton({
|
||||
accessibilityLabel,
|
||||
onPress,
|
||||
active = false,
|
||||
disabled = false
|
||||
disabled = false,
|
||||
iconColor
|
||||
}: NavBarButtonProps) {
|
||||
useMobileColorScheme()
|
||||
return (
|
||||
@@ -44,7 +47,11 @@ export function NavBarButton({
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityState={{ disabled, selected: active }}
|
||||
>
|
||||
<Icon size={18} color={active ? colors.bgBase : colors.textPrimary} strokeWidth={2.1} />
|
||||
<Icon
|
||||
size={18}
|
||||
color={active ? colors.bgBase : (iconColor ?? colors.textPrimary)}
|
||||
strokeWidth={2.1}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -196,7 +196,9 @@ export function PullDownMenu({ visible, anchorRef, groups, onClose }: PullDownMe
|
||||
disabled={action.disabled || action.loading}
|
||||
onPress={() => press(action)}
|
||||
accessibilityRole="menuitem"
|
||||
accessibilityLabel={action.label}
|
||||
accessibilityLabel={
|
||||
action.status ? `${action.label}, ${action.status}` : action.label
|
||||
}
|
||||
accessibilityHint={action.hint}
|
||||
accessibilityState={
|
||||
action.checked === undefined ? undefined : { checked: action.checked }
|
||||
@@ -213,6 +215,11 @@ export function PullDownMenu({ visible, anchorRef, groups, onClose }: PullDownMe
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
{action.status ? (
|
||||
<Text style={styles.status} numberOfLines={1}>
|
||||
{action.status}
|
||||
</Text>
|
||||
) : null}
|
||||
{action.checked ? (
|
||||
<Check size={17} color={tint} strokeWidth={2.2} />
|
||||
) : action.renderIcon ? (
|
||||
@@ -280,5 +287,12 @@ const styles = themedStyleSheet(() => ({
|
||||
hint: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: typography.metaSize
|
||||
},
|
||||
status: {
|
||||
marginLeft: 'auto',
|
||||
flexShrink: 0,
|
||||
color: colors.textSecondary,
|
||||
fontSize: typography.metaSize,
|
||||
fontVariant: ['tabular-nums']
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -17,6 +17,9 @@ export const prTabStyles = themedStyleSheet(() => ({
|
||||
header: { gap: spacing.xs },
|
||||
metaRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, flexWrap: 'wrap' },
|
||||
badge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 1,
|
||||
borderRadius: radii.pill,
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
GitMerge,
|
||||
GitPullRequest,
|
||||
GitPullRequestClosed,
|
||||
GitPullRequestDraft,
|
||||
type LucideIcon
|
||||
} from 'lucide-react-native'
|
||||
import type { MobileStatusToken } from './pr-sidebar/pr-checks-presentation'
|
||||
|
||||
// Share status tokens between workspace PR badge and sidebar to prevent color drift, matching desktop ReviewIcon.
|
||||
@@ -14,3 +21,17 @@ export function prStateToken(state: string): MobileStatusToken {
|
||||
return 'textSecondary'
|
||||
}
|
||||
}
|
||||
|
||||
// Glyph per state, like GitHub and desktop's getReviewStateIcon; open and unknown keep the plain PR glyph.
|
||||
export function prStateIcon(state: string): LucideIcon {
|
||||
switch (state.toLowerCase()) {
|
||||
case 'merged':
|
||||
return GitMerge
|
||||
case 'closed':
|
||||
return GitPullRequestClosed
|
||||
case 'draft':
|
||||
return GitPullRequestDraft
|
||||
default:
|
||||
return GitPullRequest
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GenerationFileSystem } from '../mobile-web-shell/generation-store-file-system'
|
||||
import type { GenerationFileSystem } from '../mobile-web-shell/generations/generation-store-file-system'
|
||||
import type { DeviceStoreRoots } from './device-store-files'
|
||||
import type { DeviceStoreKeychain } from './device-store-key'
|
||||
import type { DeviceStoreTable } from './device-store-seal'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { joinUri } from '../mobile-web-shell/generation-cache-uri'
|
||||
import { joinUri } from '../mobile-web-shell/generations/generation-cache-uri'
|
||||
import type {
|
||||
GenerationDirectoryEntry,
|
||||
GenerationFileListing,
|
||||
GenerationFileSystem
|
||||
} from '../mobile-web-shell/generation-store-file-system'
|
||||
import { deriveHostCacheKey } from '../mobile-web-shell/host-cache-key'
|
||||
} from '../mobile-web-shell/generations/generation-store-file-system'
|
||||
import { deriveHostCacheKey } from '../mobile-web-shell/generations/host-cache-key'
|
||||
import { DEVICE_STORE_BUCKETS, DRAFT_TMP_SUFFIX, promoteLoneDraftTmp } from './device-store-files'
|
||||
import type { DeviceStoreTable } from './device-store-seal'
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { sha256 } from '@noble/hashes/sha256'
|
||||
import { bytesToHex } from '@noble/hashes/utils'
|
||||
import type { AESEncryptionKey } from 'expo-crypto'
|
||||
import { joinUri } from '../mobile-web-shell/generation-cache-uri'
|
||||
import type { GenerationFileSystem } from '../mobile-web-shell/generation-store-file-system'
|
||||
import { deriveHostCacheKey } from '../mobile-web-shell/host-cache-key'
|
||||
import { joinUri } from '../mobile-web-shell/generations/generation-cache-uri'
|
||||
import type { GenerationFileSystem } from '../mobile-web-shell/generations/generation-store-file-system'
|
||||
import { deriveHostCacheKey } from '../mobile-web-shell/generations/host-cache-key'
|
||||
import {
|
||||
openDeviceStoreValue,
|
||||
type DeviceStoreTable,
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
import type { AESEncryptionKey } from 'expo-crypto'
|
||||
import { Directory, Paths } from 'expo-file-system'
|
||||
import { AppState } from 'react-native'
|
||||
import { createExpoGenerationFileSystem } from '../mobile-web-shell/generation-store-file-system'
|
||||
import { createExpoGenerationFileSystem } from '../mobile-web-shell/generations/generation-store-file-system'
|
||||
import { loadStoredHostProfiles } from '../host-catalog/host-metadata-store'
|
||||
import {
|
||||
readPairingKeychainItem,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Text, View } from 'react-native'
|
||||
import type { MobileWebShellUpdateFailure } from '../mobile-web-shell/mobile-web-shell-update-failure'
|
||||
import { processGenerationStore } from '../mobile-web-shell/process-generation-store'
|
||||
import { processGenerationStore } from '../mobile-web-shell/generations/process-generation-store'
|
||||
import { loadHosts } from '../host-catalog/host-store'
|
||||
import { formatUpdateFailure } from './mobile-web-shell-update-failure-copy'
|
||||
import { useMobileColorScheme } from '../theme/use-mobile-color-scheme'
|
||||
|
||||
@@ -11,6 +11,7 @@ export type MobileSyntaxTokenKind =
|
||||
| 'function'
|
||||
| 'variable'
|
||||
| 'meta'
|
||||
| 'tag'
|
||||
|
||||
export type MobileSyntaxSegment = {
|
||||
text: string
|
||||
@@ -184,28 +185,35 @@ function tokenKindForClasses(className: unknown): MobileSyntaxTokenKind | null {
|
||||
: []
|
||||
|
||||
const tokens = new Set(classes.map((value) => value.replace(/^hljs-/, '')))
|
||||
// Buckets follow highlight.js's GitHub theme, except `attr` (JSON/YAML keys) is green as on github.com.
|
||||
if (tokens.has('subst')) {
|
||||
return 'plain'
|
||||
}
|
||||
if (hasAny(tokens, ['comment', 'quote'])) {
|
||||
return 'comment'
|
||||
}
|
||||
if (hasAny(tokens, ['keyword', 'selector-tag', 'tag', 'name'])) {
|
||||
if (hasAny(tokens, ['keyword', 'doctag', 'template-tag', 'language_'])) {
|
||||
return 'keyword'
|
||||
}
|
||||
if (hasAny(tokens, ['name', 'selector-tag', 'selector-pseudo', 'attr'])) {
|
||||
return 'tag'
|
||||
}
|
||||
if (hasAny(tokens, ['string', 'regexp', 'symbol', 'bullet'])) {
|
||||
return 'string'
|
||||
}
|
||||
if (hasAny(tokens, ['number', 'literal'])) {
|
||||
if (hasAny(tokens, ['number', 'literal', 'attribute', 'selector-class', 'selector-id'])) {
|
||||
return 'number'
|
||||
}
|
||||
if (hasAny(tokens, ['type', 'built_in', 'class', 'title.class'])) {
|
||||
if (hasAny(tokens, ['type', 'built_in'])) {
|
||||
return 'type'
|
||||
}
|
||||
if (hasAny(tokens, ['title.function', 'function', 'title'])) {
|
||||
if (hasAny(tokens, ['title', 'function'])) {
|
||||
return 'function'
|
||||
}
|
||||
if (hasAny(tokens, ['attr', 'attribute', 'property', 'variable', 'params'])) {
|
||||
if (hasAny(tokens, ['variable', 'template-variable'])) {
|
||||
return 'variable'
|
||||
}
|
||||
if (hasAny(tokens, ['meta', 'doctag', 'subst', 'section'])) {
|
||||
if (hasAny(tokens, ['meta', 'section'])) {
|
||||
return 'meta'
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Platform, StyleSheet } from 'react-native'
|
||||
import { StyleSheet } from 'react-native'
|
||||
|
||||
import { colors, spacing, radii, typography } from '../../theme/mobile-theme'
|
||||
import { themedStyleSheet } from '../../theme/themed-styles'
|
||||
@@ -14,7 +14,7 @@ export const mobileSessionReaderStyles = themedStyleSheet(() => ({
|
||||
paddingBottom: spacing.xl * 3,
|
||||
fontSize: typography.bodySize,
|
||||
lineHeight: 22,
|
||||
fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' })
|
||||
fontFamily: typography.monoFamily
|
||||
},
|
||||
filePreviewScroll: {
|
||||
flex: 1,
|
||||
@@ -30,7 +30,7 @@ export const mobileSessionReaderStyles = themedStyleSheet(() => ({
|
||||
color: colors.textPrimary,
|
||||
fontSize: typography.bodySize,
|
||||
lineHeight: 22,
|
||||
fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' })
|
||||
fontFamily: typography.monoFamily
|
||||
},
|
||||
imagePreviewContainer: {
|
||||
flex: 1,
|
||||
@@ -106,14 +106,14 @@ export const mobileSessionReaderStyles = themedStyleSheet(() => ({
|
||||
color: colors.textMuted,
|
||||
fontSize: typography.metaSize,
|
||||
lineHeight: 22,
|
||||
fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' })
|
||||
fontFamily: typography.monoFamily
|
||||
},
|
||||
diffText: {
|
||||
flex: 1,
|
||||
color: colors.textPrimary,
|
||||
fontSize: typography.bodySize,
|
||||
lineHeight: 22,
|
||||
fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' })
|
||||
fontFamily: typography.monoFamily
|
||||
},
|
||||
diffPrefix: {
|
||||
color: colors.textMuted
|
||||
|
||||
@@ -7,13 +7,13 @@ import Animated, {
|
||||
useSharedValue,
|
||||
withTiming
|
||||
} from 'react-native-reanimated'
|
||||
import { usageBarFill } from '../../accounts/usage/UsageBar'
|
||||
import {
|
||||
getUsageBarState,
|
||||
getWindowResetLabel,
|
||||
usageBarFill,
|
||||
type AccountsSnapshot,
|
||||
type ProviderKey
|
||||
} from '../../components/AccountUsage'
|
||||
} from '../../accounts/usage/usage-selectors'
|
||||
import type { AccountsSnapshot } from '../../accounts/accounts-snapshot'
|
||||
import { ClaudeIcon, OpenAIIcon } from '../../components/AgentIcons'
|
||||
import { useNow } from '../../hooks/use-now'
|
||||
import { useMobileTranslation } from '../../i18n/use-mobile-translation'
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type {
|
||||
AccountProviderRateLimits,
|
||||
AccountsSnapshot
|
||||
} from '../../components/account-usage-state'
|
||||
import type { AccountProviderRateLimits, AccountsSnapshot } from '../../accounts/accounts-snapshot'
|
||||
import { selectHomeUsageCards } from './home-usage-cards'
|
||||
|
||||
function limits(
|
||||
|
||||
@@ -3,11 +3,10 @@ import {
|
||||
getInactiveProviderUsage,
|
||||
hasActiveProviderUsage,
|
||||
hasRenderableUsage,
|
||||
type AccountsSnapshot,
|
||||
type ProviderKey,
|
||||
type ProviderRateLimits
|
||||
} from '../../components/account-usage-state'
|
||||
import { getActiveCodexAccountIdForRateLimitTarget } from '../../components/codex-reset-credit'
|
||||
type ProviderKey
|
||||
} from '../../accounts/usage/usage-selectors'
|
||||
import type { AccountsSnapshot, ProviderRateLimits } from '../../accounts/accounts-snapshot'
|
||||
import { getActiveCodexAccountIdForRateLimitTarget } from '../../accounts/usage/codex-reset-credit'
|
||||
|
||||
export type HomeUsageCard = {
|
||||
/** The managed account's id, or null for the agent's own login (System default). */
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
leaveHomeSparePage,
|
||||
prepareHomeSparePage
|
||||
} from '../mobile-web-shell/home-spare-page'
|
||||
import { processGenerationStore } from '../mobile-web-shell/process-generation-store'
|
||||
import { processGenerationStore } from '../mobile-web-shell/generations/process-generation-store'
|
||||
|
||||
// Past Home's first paint and its first reads, so the boot never competes with them.
|
||||
const HOME_SPARE_IDLE_MS = 1000
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '../agents/agent-list-rows'
|
||||
import { useAgentActivityWrites } from '../agents/use-agent-activity-writes'
|
||||
import { accountsSnapshotOptions } from '../accounts/accounts-snapshot-stream'
|
||||
import type { AccountsSnapshot } from '../components/AccountUsage'
|
||||
import type { AccountsSnapshot } from '../accounts/accounts-snapshot'
|
||||
import {
|
||||
HOST_LIST_TARGET,
|
||||
homeWorktreeCountsKey,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { wipeHostStore } from '../device-store/device-store'
|
||||
import {
|
||||
deleteHostPageCache,
|
||||
forgetHostUpdateFailures
|
||||
} from '../mobile-web-shell/removed-host-shell-cache'
|
||||
} from '../mobile-web-shell/generations/removed-host-shell-cache'
|
||||
import { unregisterPushForRemovedHost } from '../notifications/push-registration'
|
||||
import { forgetNativeHostQueries } from '../query/native-query-persistence'
|
||||
import { forgetHostDescriptor } from '../host-descriptors/host-descriptor-store'
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const asyncStorage = vi.hoisted(() => ({
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@react-native-async-storage/async-storage', () => ({ default: asyncStorage }))
|
||||
|
||||
import {
|
||||
loadMobileRelayHostRoutingState,
|
||||
resetMobileRelayHostOverlayStoreForTests,
|
||||
saveMobileRelayHostRouting
|
||||
} from './mobile-relay-host-overlay-store'
|
||||
import type { MobileRelayHostOverlay } from './mobile-relay-host-overlay'
|
||||
|
||||
const STORAGE_KEY = 'orca:mobile-relay:host-overlays:v2'
|
||||
const RELAY = {
|
||||
v: 1 as const,
|
||||
directorUrl: 'https://relay.onorca.dev',
|
||||
cellUrl: 'https://relay-c1.onorca.dev',
|
||||
assignmentEpoch: 7,
|
||||
relayHostId: 'AbCdEf0123_-xyZ9',
|
||||
e2eeFraming: 2 as const
|
||||
}
|
||||
const RELAY_ONLY_OVERLAY: MobileRelayHostOverlay = {
|
||||
v: 2,
|
||||
hostId: 'host-1',
|
||||
endpoints: [
|
||||
{
|
||||
id: 'relay-primary',
|
||||
kind: 'relay',
|
||||
url: 'wss://relay-c1.onorca.dev/v1/connect/AbCdEf0123_-xyZ9'
|
||||
}
|
||||
],
|
||||
relayHostId: RELAY.relayHostId,
|
||||
relay: RELAY
|
||||
}
|
||||
// What builds before the relay-routing split wrote: a copy of the row's address beside the relay.
|
||||
const LEGACY_OVERLAY: MobileRelayHostOverlay = {
|
||||
...RELAY_ONLY_OVERLAY,
|
||||
endpoints: [
|
||||
{ id: 'direct-primary', kind: 'lan', url: 'ws://192.168.1.10:6768' },
|
||||
...RELAY_ONLY_OVERLAY.endpoints
|
||||
]
|
||||
}
|
||||
|
||||
// Freeze the shipped overlay schema to prove older builds still accept written records.
|
||||
|
||||
describe('mobile relay host overlay store', () => {
|
||||
let stored: string | null
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
resetMobileRelayHostOverlayStoreForTests()
|
||||
stored = null
|
||||
asyncStorage.getItem.mockImplementation(async (key: string) =>
|
||||
key === STORAGE_KEY ? stored : null
|
||||
)
|
||||
asyncStorage.setItem.mockImplementation(async (key: string, value: string) => {
|
||||
if (key === STORAGE_KEY) {
|
||||
stored = value
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores a direct address an older build stored and drops it on the next routing write', async () => {
|
||||
stored = JSON.stringify([LEGACY_OVERLAY])
|
||||
|
||||
await expect(loadMobileRelayHostRoutingState(new Set(['host-1']))).resolves.toEqual({
|
||||
relays: new Map([['host-1', RELAY]]),
|
||||
orphanHostIds: []
|
||||
})
|
||||
await expect(saveMobileRelayHostRouting('host-1', RELAY)).resolves.toBe(true)
|
||||
expect(JSON.parse(stored!)).toEqual([RELAY_ONLY_OVERLAY])
|
||||
})
|
||||
})
|
||||
@@ -1,101 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const asyncStorage = vi.hoisted(() => ({
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn()
|
||||
}))
|
||||
const secureStore = vi.hoisted(() => ({
|
||||
getItemAsync: vi.fn(),
|
||||
setItemAsync: vi.fn(),
|
||||
deleteItemAsync: vi.fn()
|
||||
}))
|
||||
const platform = vi.hoisted(() => ({ OS: 'ios' }))
|
||||
|
||||
vi.mock('@react-native-async-storage/async-storage', () => ({ default: asyncStorage }))
|
||||
vi.mock('expo-secure-store', () => ({
|
||||
WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'WHEN_UNLOCKED_THIS_DEVICE_ONLY',
|
||||
...secureStore
|
||||
}))
|
||||
vi.mock('react-native', () => ({ Platform: platform }))
|
||||
|
||||
import { promotePairingJournalCredential } from './mobile-relay-credential-bundle'
|
||||
import type { MobileRelayPairingJournal } from '../pairing/mobile-relay-pairing-journal'
|
||||
import { resetPairingKeychainForTests } from './pairing-keychain'
|
||||
|
||||
const journal = {
|
||||
metadata: {
|
||||
v: 1,
|
||||
journalId: 'pair-1',
|
||||
offerFingerprint: 'A'.repeat(43),
|
||||
host: {
|
||||
id: 'host-1',
|
||||
name: 'Blue Whale',
|
||||
endpoint: 'ws://192.168.1.10:6768',
|
||||
publicKeyB64: 'A'.repeat(44),
|
||||
lastConnected: 1
|
||||
},
|
||||
relay: {
|
||||
v: 1,
|
||||
directorUrl: 'https://relay.onorca.dev',
|
||||
cellUrl: 'https://relay-c1.onorca.dev',
|
||||
assignmentEpoch: 7,
|
||||
relayHostId: 'AbCdEf0123_-xyZ9',
|
||||
inviteExpiresAt: 10_000,
|
||||
e2eeFraming: 2
|
||||
},
|
||||
installReqId: 'install-1',
|
||||
resumeConfirmReqId: 'confirm-1',
|
||||
pendingResumeTokenHash: 'B'.repeat(43),
|
||||
winner: 'direct',
|
||||
authorizationMode: 'authenticated-direct'
|
||||
},
|
||||
secrets: {
|
||||
v: 1,
|
||||
journalId: 'pair-1',
|
||||
deviceToken: 'device-token',
|
||||
inviteToken: 'C'.repeat(43),
|
||||
pendingResumeToken: 'D'.repeat(43)
|
||||
}
|
||||
} satisfies MobileRelayPairingJournal
|
||||
|
||||
describe('mobile relay credential bundle', () => {
|
||||
const stored = new Map<string, string>()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
resetPairingKeychainForTests()
|
||||
platform.OS = 'ios'
|
||||
stored.clear()
|
||||
asyncStorage.getItem.mockResolvedValue(null)
|
||||
asyncStorage.setItem.mockResolvedValue(undefined)
|
||||
secureStore.getItemAsync.mockImplementation(async (key: string) => stored.get(key) ?? null)
|
||||
secureStore.setItemAsync.mockImplementation(async (key: string, value: string) => {
|
||||
stored.set(key, value)
|
||||
})
|
||||
secureStore.deleteItemAsync.mockImplementation(async (key: string) => {
|
||||
stored.delete(key)
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a result from another request or authorization mode', () => {
|
||||
const installed = {
|
||||
v: 1 as const,
|
||||
reqId: 'install-1',
|
||||
authorizationMode: 'authenticated-direct' as const,
|
||||
currentVersion: 1,
|
||||
resumeExpiresAt: 50_000
|
||||
}
|
||||
expect(() =>
|
||||
promotePairingJournalCredential({
|
||||
journal,
|
||||
installed: { ...installed, reqId: 'other-request' }
|
||||
})
|
||||
).toThrow(/does not match/)
|
||||
expect(() =>
|
||||
promotePairingJournalCredential({
|
||||
journal,
|
||||
installed: { ...installed, authorizationMode: 'relay-basis' }
|
||||
})
|
||||
).toThrow(/does not match/)
|
||||
})
|
||||
})
|
||||
@@ -5,9 +5,6 @@ import {
|
||||
CircleDot,
|
||||
ExternalLink,
|
||||
GitMerge,
|
||||
GitPullRequest,
|
||||
GitPullRequestClosed,
|
||||
GitPullRequestDraft,
|
||||
SquareTerminal,
|
||||
StickyNote
|
||||
} from 'lucide-react-native'
|
||||
@@ -15,6 +12,7 @@ import { colors, spacing, typography } from '../theme/mobile-theme'
|
||||
import { themedStyleSheet } from '../theme/themed-styles'
|
||||
import { useMobileColorScheme } from '../theme/use-mobile-color-scheme'
|
||||
import { prStateColor } from '../components/WorktreeMetaGlyphs'
|
||||
import { PrStateIcon } from '../components/PrStateIcon'
|
||||
import {
|
||||
LIST_LEADING_INSET,
|
||||
LIST_LEADING_SLOT,
|
||||
@@ -135,20 +133,13 @@ function originLabel(origin: WorkspaceOrigin, t: Translate): string {
|
||||
|
||||
function ReviewIcon({ item }: { item: WorkspaceDetailLink }) {
|
||||
useMobileColorScheme()
|
||||
const color = prStateColor(item.state ?? '')
|
||||
if (
|
||||
item.state === 'merged' ||
|
||||
(item.type === 'mr' && item.state !== 'closed' && item.state !== 'draft')
|
||||
) {
|
||||
const state = item.state ?? ''
|
||||
const color = prStateColor(state)
|
||||
// GitLab draws every open MR with the merge glyph.
|
||||
if (item.type === 'mr' && state !== 'closed' && state !== 'draft') {
|
||||
return <GitMerge size={ICON_SIZE} color={color} />
|
||||
}
|
||||
if (item.state === 'closed') {
|
||||
return <GitPullRequestClosed size={ICON_SIZE} color={color} />
|
||||
}
|
||||
if (item.state === 'draft') {
|
||||
return <GitPullRequestDraft size={ICON_SIZE} color={color} />
|
||||
}
|
||||
return <GitPullRequest size={ICON_SIZE} color={color} />
|
||||
return <PrStateIcon state={state} size={ICON_SIZE} color={color} />
|
||||
}
|
||||
|
||||
function LinkIcon({ item }: { item: WorkspaceDetailLink }) {
|
||||
|
||||
@@ -2454,7 +2454,8 @@
|
||||
"tabStrip": {
|
||||
"checkingCapabilities": "Desktop-Funktionen werden geprüft – gleich erneut versuchen",
|
||||
"newTab": "Neuen Tab erstellen",
|
||||
"quickCommandsUpdateRequired": "Desktop-Update für Schnellbefehle erforderlich"
|
||||
"quickCommandsUpdateRequired": "Desktop-Update für Schnellbefehle erforderlich",
|
||||
"tabStatus": "{{title}}, {{status}}"
|
||||
},
|
||||
"terminalActionSheetActions": {
|
||||
"clearTerminal": "Terminal leeren",
|
||||
@@ -3364,6 +3365,12 @@
|
||||
},
|
||||
"tasks": {
|
||||
"hostedMerge": {
|
||||
"confirmGitHubMerge": "PR #{{number}} zusammenführen?",
|
||||
"confirmGitHubSquash": "PR #{{number}} mit Squash zusammenführen?",
|
||||
"confirmGitHubRebase": "PR #{{number}} mit Rebase zusammenführen?",
|
||||
"confirmGitLabMerge": "Merge Request #{{number}} zusammenführen?",
|
||||
"confirmGitLabSquash": "Merge Request #{{number}} mit Squash zusammenführen?",
|
||||
"confirmGitLabRebase": "Merge Request #{{number}} mit Rebase zusammenführen?",
|
||||
"merged": "Zusammengeführt",
|
||||
"closed": "Geschlossen",
|
||||
"conflicts": "Konflikte",
|
||||
|
||||
@@ -820,6 +820,8 @@
|
||||
"webShell": {
|
||||
"shellStateScreen": {
|
||||
"isolationUnavailable": "This device's WebView is too old to open the workspace safely.",
|
||||
"isolationUnavailableAndroid": "Orca needs Android System WebView or Chrome 101 or newer. Update it from the Play Store, or pick a newer one in Developer options > WebView implementation.",
|
||||
"webViewInUse": "This device has {{name}} {{version}}.",
|
||||
"downloadFailed": "The latest app could not be downloaded from this host.",
|
||||
"hostProfileUnreadable": "Could not read this host on your device. Try again.",
|
||||
"statusUnreadable": "Could not read this host's status. Go back and reopen it.",
|
||||
@@ -2182,10 +2184,10 @@
|
||||
"renameTerminal": "Rename Terminal",
|
||||
"renameTerminalFailed": "Couldn't rename the terminal. {{reason}}",
|
||||
"terminalName": "Terminal name",
|
||||
"allowAutoClose": "Allow Auto-Close",
|
||||
"agents": "Agents"
|
||||
"allowAutoClose": "Allow Auto-Close"
|
||||
},
|
||||
"tabStrip": {
|
||||
"tabStatus": "{{title}}, {{status}}",
|
||||
"newTab": "New tab",
|
||||
"quickCommandsUpdateRequired": "Desktop update required for quick commands",
|
||||
"checkingCapabilities": "Checking desktop capabilities — try again in a moment"
|
||||
@@ -2887,6 +2889,8 @@
|
||||
},
|
||||
"presentation": {
|
||||
"loadingTabs": "Loading tabs",
|
||||
"oneTab": "1 tab",
|
||||
"tabCount": "{{tabCount}} tabs",
|
||||
"tapToRetry": "{{status}} — tap to retry"
|
||||
},
|
||||
"documentReaders": {
|
||||
@@ -2924,29 +2928,6 @@
|
||||
"skip": "Skip",
|
||||
"stop": "Stop",
|
||||
"yes": "Yes"
|
||||
},
|
||||
"tabsSheet": {
|
||||
"needsYou": "Needs you",
|
||||
"working": "Working",
|
||||
"everythingElse": "Everything else",
|
||||
"open": "Open",
|
||||
"closeTab": "Close {{title}}"
|
||||
},
|
||||
"tabRows": {
|
||||
"browser": "Browser",
|
||||
"markdown": "Markdown note",
|
||||
"file": "File",
|
||||
"chat": "Chat",
|
||||
"terminal": "Terminal"
|
||||
},
|
||||
"tabBar": {
|
||||
"oneTab": "1 tab",
|
||||
"tabCount": "{{count}} tabs",
|
||||
"anotherNeedsYou": "another tab needs you",
|
||||
"actionsHint": "Shows actions for this tab",
|
||||
"next": "Next tab",
|
||||
"previous": "Previous tab",
|
||||
"hint": "Shows all tabs"
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
@@ -3482,7 +3463,12 @@
|
||||
"commitThenPullPush": "Commit, then pull and push",
|
||||
"resolveConflicts": "Resolve conflicts before committing",
|
||||
"stageFile": "Stage at least one file to commit",
|
||||
"enterMessage": "Enter a commit message to commit"
|
||||
"enterMessage": "Enter a commit message to commit",
|
||||
"statusConflicts": "Conflicts",
|
||||
"statusNothingStaged": "Nothing staged",
|
||||
"statusNeedsMessage": "Needs message",
|
||||
"statusNotPublished": "Not published",
|
||||
"statusNoBranch": "No branch"
|
||||
},
|
||||
"createPrAction": {
|
||||
"createReview": "Create {{titleLabel}}",
|
||||
@@ -3559,7 +3545,15 @@
|
||||
"nothingToPushTo": "Nothing to push to {{upstreamName}}",
|
||||
"nothingToForcePushTo": "Nothing to force push to {{upstreamName}}",
|
||||
"rebaseFromNamedBase": "Rebase from {{baseRef}}",
|
||||
"rebaseWithLatestFrom": "Rebase current branch with latest commits from {{baseRef}}"
|
||||
"rebaseWithLatestFrom": "Rebase current branch with latest commits from {{baseRef}}",
|
||||
"statusUpToDate": "Up to date",
|
||||
"statusSetsUpstream": "Sets upstream",
|
||||
"statusNotPublished": "Not published",
|
||||
"statusNoBranch": "No branch",
|
||||
"statusMayFail": "May fail",
|
||||
"statusPublished": "Published",
|
||||
"statusStashFirst": "Stash first",
|
||||
"statusReplaces": "Replaces ↓{{behind}}"
|
||||
},
|
||||
"actions": {
|
||||
"createPr": "Create PR",
|
||||
@@ -4559,6 +4553,12 @@
|
||||
"createFailed": "Failed to create workspace"
|
||||
},
|
||||
"hostedMerge": {
|
||||
"confirmGitHubMerge": "Merge PR #{{number}}?",
|
||||
"confirmGitHubSquash": "Squash and merge PR #{{number}}?",
|
||||
"confirmGitHubRebase": "Rebase and merge PR #{{number}}?",
|
||||
"confirmGitLabMerge": "Merge merge request #{{number}}?",
|
||||
"confirmGitLabSquash": "Squash and merge merge request #{{number}}?",
|
||||
"confirmGitLabRebase": "Rebase and merge merge request #{{number}}?",
|
||||
"merged": "Merged",
|
||||
"closed": "Closed",
|
||||
"conflicts": "Conflicts",
|
||||
|
||||
@@ -2524,7 +2524,8 @@
|
||||
"tabStrip": {
|
||||
"checkingCapabilities": "Comprobando las funciones del escritorio. Inténtalo de nuevo en un momento.",
|
||||
"newTab": "Nueva pestaña",
|
||||
"quickCommandsUpdateRequired": "Actualiza el escritorio para usar los comandos rápidos."
|
||||
"quickCommandsUpdateRequired": "Actualiza el escritorio para usar los comandos rápidos.",
|
||||
"tabStatus": "{{title}}, {{status}}"
|
||||
},
|
||||
"terminalActionSheetActions": {
|
||||
"clearTerminal": "Limpiar terminal",
|
||||
@@ -3452,6 +3453,12 @@
|
||||
},
|
||||
"tasks": {
|
||||
"hostedMerge": {
|
||||
"confirmGitHubMerge": "¿Fusionar el PR #{{number}}?",
|
||||
"confirmGitHubSquash": "¿Compactar y fusionar el PR #{{number}}?",
|
||||
"confirmGitHubRebase": "¿Hacer rebase y fusionar el PR #{{number}}?",
|
||||
"confirmGitLabMerge": "¿Fusionar la solicitud de merge #{{number}}?",
|
||||
"confirmGitLabSquash": "¿Compactar y fusionar la solicitud de merge #{{number}}?",
|
||||
"confirmGitLabRebase": "¿Hacer rebase y fusionar la solicitud de merge #{{number}}?",
|
||||
"merged": "Fusionado",
|
||||
"closed": "Cerrado",
|
||||
"conflicts": "Conflictos",
|
||||
|
||||
@@ -2524,7 +2524,8 @@
|
||||
"tabStrip": {
|
||||
"checkingCapabilities": "Vérification des fonctionnalités de l'ordinateur — réessayez dans un instant",
|
||||
"newTab": "Nouvel onglet",
|
||||
"quickCommandsUpdateRequired": "Mettez à jour Orca sur l'ordinateur pour utiliser les commandes rapides"
|
||||
"quickCommandsUpdateRequired": "Mettez à jour Orca sur l'ordinateur pour utiliser les commandes rapides",
|
||||
"tabStatus": "{{title}}, {{status}}"
|
||||
},
|
||||
"terminalActionSheetActions": {
|
||||
"clearTerminal": "Effacer le terminal",
|
||||
@@ -3452,6 +3453,12 @@
|
||||
},
|
||||
"tasks": {
|
||||
"hostedMerge": {
|
||||
"confirmGitHubMerge": "Fusionner la PR nº {{number}} ?",
|
||||
"confirmGitHubSquash": "Compresser les commits et fusionner la PR nº {{number}} ?",
|
||||
"confirmGitHubRebase": "Rebaser et fusionner la PR nº {{number}} ?",
|
||||
"confirmGitLabMerge": "Fusionner la demande de fusion nº {{number}} ?",
|
||||
"confirmGitLabSquash": "Compresser les commits et fusionner la demande de fusion nº {{number}} ?",
|
||||
"confirmGitLabRebase": "Rebaser et fusionner la demande de fusion nº {{number}} ?",
|
||||
"merged": "Fusionné",
|
||||
"closed": "Fermé",
|
||||
"conflicts": "Conflits",
|
||||
|
||||
@@ -2450,7 +2450,8 @@
|
||||
"tabStrip": {
|
||||
"checkingCapabilities": "デスクトップの対応機能を確認中です。少し待ってから再試行してください。",
|
||||
"newTab": "新規タブ",
|
||||
"quickCommandsUpdateRequired": "クイックコマンドの利用にはデスクトップの更新が必要です。"
|
||||
"quickCommandsUpdateRequired": "クイックコマンドの利用にはデスクトップの更新が必要です。",
|
||||
"tabStatus": "{{title}}、{{status}}"
|
||||
},
|
||||
"terminalActionSheetActions": {
|
||||
"clearTerminal": "ターミナルをクリア",
|
||||
@@ -3440,6 +3441,12 @@
|
||||
"openView": "GitHub でビューを開く"
|
||||
},
|
||||
"hostedMerge": {
|
||||
"confirmGitHubMerge": "PR #{{number}} をマージしますか?",
|
||||
"confirmGitHubSquash": "PR #{{number}} のコミットをまとめてマージしますか?",
|
||||
"confirmGitHubRebase": "PR #{{number}} をリベースしてマージしますか?",
|
||||
"confirmGitLabMerge": "マージリクエスト #{{number}} をマージしますか?",
|
||||
"confirmGitLabSquash": "マージリクエスト #{{number}} のコミットをまとめてマージしますか?",
|
||||
"confirmGitLabRebase": "マージリクエスト #{{number}} をリベースしてマージしますか?",
|
||||
"merged": "マージ済み",
|
||||
"closed": "クローズ済み",
|
||||
"conflicts": "競合あり",
|
||||
|
||||
@@ -2450,7 +2450,8 @@
|
||||
"tabStrip": {
|
||||
"checkingCapabilities": "데스크톱에서 지원하는 기능을 확인 중입니다. 잠시 후 다시 시도하세요.",
|
||||
"newTab": "새 탭",
|
||||
"quickCommandsUpdateRequired": "빠른 명령을 사용하려면 데스크톱 업데이트가 필요합니다."
|
||||
"quickCommandsUpdateRequired": "빠른 명령을 사용하려면 데스크톱 업데이트가 필요합니다.",
|
||||
"tabStatus": "{{title}}, {{status}}"
|
||||
},
|
||||
"terminalActionSheetActions": {
|
||||
"clearTerminal": "터미널 지우기",
|
||||
@@ -3360,6 +3361,12 @@
|
||||
},
|
||||
"tasks": {
|
||||
"hostedMerge": {
|
||||
"confirmGitHubMerge": "PR #{{number}}을(를) 병합하시겠습니까?",
|
||||
"confirmGitHubSquash": "PR #{{number}}의 커밋을 합쳐서 병합하시겠습니까?",
|
||||
"confirmGitHubRebase": "PR #{{number}}을(를) 리베이스한 후 병합하시겠습니까?",
|
||||
"confirmGitLabMerge": "병합 요청 #{{number}}을(를) 병합하시겠습니까?",
|
||||
"confirmGitLabSquash": "병합 요청 #{{number}}의 커밋을 합쳐서 병합하시겠습니까?",
|
||||
"confirmGitLabRebase": "병합 요청 #{{number}}을(를) 리베이스한 후 병합하시겠습니까?",
|
||||
"merged": "병합됨",
|
||||
"closed": "닫힘",
|
||||
"conflicts": "충돌",
|
||||
|
||||
+678
-79
File diff suppressed because it is too large
Load Diff
@@ -2594,7 +2594,8 @@
|
||||
"tabStrip": {
|
||||
"checkingCapabilities": "Проверяем возможности Orca на компьютере — повторите попытку чуть позже",
|
||||
"newTab": "Новая вкладка",
|
||||
"quickCommandsUpdateRequired": "Для быстрых команд обновите Orca на компьютере"
|
||||
"quickCommandsUpdateRequired": "Для быстрых команд обновите Orca на компьютере",
|
||||
"tabStatus": "{{title}}, {{status}}"
|
||||
},
|
||||
"terminalActionSheetActions": {
|
||||
"clearTerminal": "Очистить терминал",
|
||||
@@ -3540,6 +3541,12 @@
|
||||
},
|
||||
"tasks": {
|
||||
"hostedMerge": {
|
||||
"confirmGitHubMerge": "Слить PR #{{number}}?",
|
||||
"confirmGitHubSquash": "Объединить коммиты и слить PR #{{number}}?",
|
||||
"confirmGitHubRebase": "Выполнить rebase и слить PR #{{number}}?",
|
||||
"confirmGitLabMerge": "Слить запрос на слияние #{{number}}?",
|
||||
"confirmGitLabSquash": "Объединить коммиты и слить запрос на слияние #{{number}}?",
|
||||
"confirmGitLabRebase": "Выполнить rebase и слить запрос на слияние #{{number}}?",
|
||||
"merged": "Слито",
|
||||
"closed": "Закрыто",
|
||||
"conflicts": "Конфликты",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user